fix(desktop): resolve flaky integration tests via project-level assertion timeout (#812)

This commit is contained in:
Will Pfleger
2026-06-01 18:48:37 -04:00
committed by GitHub
parent 93183cb75d
commit 6481428e2b
11 changed files with 38 additions and 57 deletions
+3
View File
@@ -48,6 +48,9 @@ export default defineConfig({
use: { use: {
...devices["Desktop Chrome"], ...devices["Desktop Chrome"],
}, },
expect: {
timeout: process.env.CI ? 15_000 : 10_000,
},
}, },
], ],
webServer: { webServer: {
@@ -11,7 +11,7 @@ export function useActiveChannelHeader(
currentPubkey?: string, currentPubkey?: string,
) { ) {
const activeDmParticipantPubkeys = React.useMemo(() => { const activeDmParticipantPubkeys = React.useMemo(() => {
if (!activeChannel || activeChannel.channelType !== "dm") { if (activeChannel?.channelType !== "dm") {
return []; return [];
} }
@@ -18,7 +18,7 @@ type TypingIndicatorRowProps = {
}; };
function resolveFallbackName(channel: Channel | null, pubkey: string) { function resolveFallbackName(channel: Channel | null, pubkey: string) {
if (!channel || channel.channelType !== "dm") { if (channel?.channelType !== "dm") {
return null; return null;
} }
@@ -20,9 +20,9 @@ test("move up succeeds: middle section swaps order with the one above", () => {
const result = swapSectionOrder(store, "b", "up"); const result = swapSectionOrder(store, "b", "up");
assert.notEqual(result, null); assert.notEqual(result, null);
const byId = Object.fromEntries(result.sections.map((s) => [s.id, s.order])); const byId = Object.fromEntries(result.sections.map((s) => [s.id, s.order]));
assert.equal(byId["b"], 0); assert.equal(byId.b, 0);
assert.equal(byId["a"], 1); assert.equal(byId.a, 1);
assert.equal(byId["c"], 2); assert.equal(byId.c, 2);
}); });
test("move down succeeds: middle section swaps order with the one below", () => { test("move down succeeds: middle section swaps order with the one below", () => {
@@ -34,9 +34,9 @@ test("move down succeeds: middle section swaps order with the one below", () =>
const result = swapSectionOrder(store, "b", "down"); const result = swapSectionOrder(store, "b", "down");
assert.notEqual(result, null); assert.notEqual(result, null);
const byId = Object.fromEntries(result.sections.map((s) => [s.id, s.order])); const byId = Object.fromEntries(result.sections.map((s) => [s.id, s.order]));
assert.equal(byId["b"], 2); assert.equal(byId.b, 2);
assert.equal(byId["c"], 1); assert.equal(byId.c, 1);
assert.equal(byId["a"], 0); assert.equal(byId.a, 0);
}); });
test("move up at top boundary returns null", () => { test("move up at top boundary returns null", () => {
@@ -73,7 +73,7 @@ test("non-contiguous orders: swap uses actual order values not indices", () => {
const result = swapSectionOrder(store, "b", "up"); const result = swapSectionOrder(store, "b", "up");
assert.notEqual(result, null); assert.notEqual(result, null);
const byId = Object.fromEntries(result.sections.map((s) => [s.id, s.order])); const byId = Object.fromEntries(result.sections.map((s) => [s.id, s.order]));
assert.equal(byId["b"], 0); assert.equal(byId.b, 0);
assert.equal(byId["a"], 5); assert.equal(byId.a, 5);
assert.equal(byId["c"], 10); assert.equal(byId.c, 10);
}); });
+1 -1
View File
@@ -90,7 +90,7 @@ export function useUserStatusSubscription() {
function handleStatusEvent(event: RelayEvent) { function handleStatusEvent(event: RelayEvent) {
if (isCancelled) return; if (isCancelled) return;
const dTag = event.tags.find((t) => t[0] === "d"); const dTag = event.tags.find((t) => t[0] === "d");
if (!dTag || dTag[1] !== "general") return; if (dTag?.[1] !== "general") return;
const parsed = parseUserStatusEvent(event); const parsed = parseUserStatusEvent(event);
const status: UserStatus | null = const status: UserStatus | null =
parsed.text || parsed.emoji parsed.text || parsed.emoji
+1 -1
View File
@@ -564,7 +564,7 @@ export class RelayClient {
return async () => { return async () => {
const active = this.subscriptions.get(subId); const active = this.subscriptions.get(subId);
if (!active || active.mode !== "live") { if (active?.mode !== "live") {
return; return;
} }
@@ -6,7 +6,7 @@ import { resolveFileCard } from "./markdownFileCard.ts";
// A generic-file URL (non-media extension) does not match the relay-media // A generic-file URL (non-media extension) does not match the relay-media
// proxy regex, so `rewriteRelayUrl` passes it through unchanged — assertions // proxy regex, so `rewriteRelayUrl` passes it through unchanged — assertions
// can compare hrefs directly. // can compare hrefs directly.
const PDF_URL = "https://relay.example/media/" + "a".repeat(64) + ".pdf"; const PDF_URL = `https://relay.example/media/${"a".repeat(64)}.pdf`;
test("resolveFileCard: returns null when there is no imeta entry", () => { test("resolveFileCard: returns null when there is no imeta entry", () => {
assert.equal(resolveFileCard(undefined, PDF_URL, ""), null); assert.equal(resolveFileCard(undefined, PDF_URL, ""), null);
@@ -62,12 +62,12 @@ test("resolveFileCard: falls back to link child text when imeta has no filename"
test("resolveFileCard: falls back to URL tail when no filename or child text", () => { test("resolveFileCard: falls back to URL tail when no filename or child text", () => {
const card = resolveFileCard({ m: "application/octet-stream" }, PDF_URL, ""); const card = resolveFileCard({ m: "application/octet-stream" }, PDF_URL, "");
assert.equal(card?.filename, "a".repeat(64) + ".pdf"); assert.equal(card?.filename, `${"a".repeat(64)}.pdf`);
}); });
test("resolveFileCard: octet-stream (no magic bytes) is treated as a file", () => { test("resolveFileCard: octet-stream (no magic bytes) is treated as a file", () => {
// Text/code/data upload with no magic signature — the Slack-like case. // Text/code/data upload with no magic signature — the Slack-like case.
const url = "https://relay.example/media/" + "b".repeat(64) + ".txt"; const url = `https://relay.example/media/${"b".repeat(64)}.txt`;
const card = resolveFileCard( const card = resolveFileCard(
{ m: "application/octet-stream", filename: "notes.txt" }, { m: "application/octet-stream", filename: "notes.txt" },
url, url,
+1 -1
View File
@@ -4298,7 +4298,7 @@ function resolveMockUploadDescriptors(
if (configured !== undefined) return configured; if (configured !== undefined) return configured;
return [ return [
{ {
url: "https://mock.relay/media/" + "a".repeat(64) + ".pdf", url: `https://mock.relay/media/${"a".repeat(64)}.pdf`,
sha256: "a".repeat(64), sha256: "a".repeat(64),
size: 12345, size: 12345,
type: "application/pdf", type: "application/pdf",
+5 -25
View File
@@ -6,7 +6,6 @@ import { assertRelaySeeded } from "../helpers/seed";
const isCi = Boolean(process.env.CI); const isCi = Boolean(process.env.CI);
const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000; const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000;
const relayDeliveryTimeoutMs = isCi ? 15_000 : 10_000;
async function createStream( async function createStream(
page: import("@playwright/test").Page, page: import("@playwright/test").Page,
@@ -276,11 +275,7 @@ test("live mentions refetch the home feed without waiting for polling", async ({
message, message,
); );
await expect await expect.poll(() => getLoggedNotificationCount(targetPage)).toBe(1);
.poll(() => getLoggedNotificationCount(targetPage), {
timeout: relayDeliveryTimeoutMs,
})
.toBe(1);
const notifications = await getLoggedNotifications(targetPage); const notifications = await getLoggedNotifications(targetPage);
@@ -300,14 +295,9 @@ test("live mentions refetch the home feed without waiting for polling", async ({
await expect(targetPage.getByTestId("home-inbox-list")).toBeVisible(); await expect(targetPage.getByTestId("home-inbox-list")).toBeVisible();
await expect(targetPage.getByTestId("home-inbox-list")).toContainText( await expect(targetPage.getByTestId("home-inbox-list")).toContainText(
message, message,
{ timeout: relayDeliveryTimeoutMs },
); );
await expect(targetPage.getByTestId("sidebar-home-count")).toHaveCount(0); await expect(targetPage.getByTestId("sidebar-home-count")).toHaveCount(0);
await expect await expect.poll(() => getLoggedNotificationCount(targetPage)).toBe(1);
.poll(() => getLoggedNotificationCount(targetPage), {
timeout: relayDeliveryTimeoutMs,
})
.toBe(1);
} finally { } finally {
await targetContext.close(); await targetContext.close();
await senderContext.close(); await senderContext.close();
@@ -345,15 +335,9 @@ test("live forum mentions refetch the home feed without waiting for polling", as
mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey], mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey],
}); });
await expect(targetPage.getByTestId("sidebar-home-count")).toHaveText("1", { await expect(targetPage.getByTestId("sidebar-home-count")).toHaveText("1");
timeout: relayDeliveryTimeoutMs,
});
await expect await expect.poll(() => getLoggedNotificationCount(targetPage)).toBe(1);
.poll(() => getLoggedNotificationCount(targetPage), {
timeout: relayDeliveryTimeoutMs,
})
.toBe(1);
const notifications = await getLoggedNotifications(targetPage); const notifications = await getLoggedNotifications(targetPage);
@@ -371,11 +355,7 @@ test("live forum mentions refetch the home feed without waiting for polling", as
message, message,
); );
await expect(targetPage.getByTestId("sidebar-home-count")).toHaveCount(0); await expect(targetPage.getByTestId("sidebar-home-count")).toHaveCount(0);
await expect await expect.poll(() => getLoggedNotificationCount(targetPage)).toBe(1);
.poll(() => getLoggedNotificationCount(targetPage), {
timeout: relayDeliveryTimeoutMs,
})
.toBe(1);
} finally { } finally {
await targetContext.close(); await targetContext.close();
await senderContext.close(); await senderContext.close();
+3 -10
View File
@@ -4,13 +4,10 @@ import { installRelayBridge, TEST_IDENTITIES } from "../helpers/bridge";
import { assertRelaySeeded } from "../helpers/seed"; import { assertRelaySeeded } from "../helpers/seed";
const isCi = Boolean(process.env.CI); const isCi = Boolean(process.env.CI);
const relayDeliveryTimeoutMs = isCi ? 15_000 : 5_000;
const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000; const relaySeedHookTimeoutMs = isCi ? 90_000 : 30_000;
async function expectTimelineToContain(page: Page, text: string) { async function expectTimelineToContain(page: Page, text: string) {
await expect(page.getByTestId("message-timeline")).toContainText(text, { await expect(page.getByTestId("message-timeline")).toContainText(text);
timeout: relayDeliveryTimeoutMs,
});
} }
async function getTimelineMetrics(page: Page) { async function getTimelineMetrics(page: Page) {
@@ -178,9 +175,7 @@ test("loads the home feed from the relay", async ({ browser }) => {
mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey], mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey],
}); });
await expect(page.getByTestId("home-inbox-list")).toContainText(message, { await expect(page.getByTestId("home-inbox-list")).toContainText(message);
timeout: relayDeliveryTimeoutMs,
});
await expect(page.getByTestId("home-inbox-detail")).toBeVisible(); await expect(page.getByTestId("home-inbox-detail")).toBeVisible();
} finally { } finally {
await targetContext.close(); await targetContext.close();
@@ -210,9 +205,7 @@ test("shows sent inbox replies immediately in the inbox detail pane", async ({
mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey], mentionPubkeys: [TEST_IDENTITIES.tyler.pubkey],
}); });
await page.getByTestId("home-inbox-list").getByText(message).click({ await page.getByTestId("home-inbox-list").getByText(message).click();
timeout: relayDeliveryTimeoutMs,
});
await expect(page.getByTestId("home-inbox-detail")).toBeVisible(); await expect(page.getByTestId("home-inbox-detail")).toBeVisible();
await expect(page.getByTestId("message-input")).toBeEnabled(); await expect(page.getByTestId("message-input")).toBeEnabled();
+10 -5
View File
@@ -42,7 +42,8 @@ export function useGitTree(
return useQuery({ return useQuery({
queryKey: ["git-tree", owner, repoName, ref, path ?? ""], queryKey: ["git-tree", owner, repoName, ref, path ?? ""],
queryFn: async () => { queryFn: async () => {
const { fs, dir } = cloneQuery.data!; if (!cloneQuery.data) throw new Error("unreachable: enabled guards data");
const { fs, dir } = cloneQuery.data;
const oid = await resolveRef({ fs, dir, ref }); const oid = await resolveRef({ fs, dir, ref });
const entries = await readTreeEntries(fs, dir, oid, path || undefined); const entries = await readTreeEntries(fs, dir, oid, path || undefined);
@@ -65,7 +66,8 @@ export function useGitLog(owner: string, repoName: string, ref: string) {
return useQuery({ return useQuery({
queryKey: ["git-log", owner, repoName, ref], queryKey: ["git-log", owner, repoName, ref],
queryFn: async () => { queryFn: async () => {
const { fs, dir } = cloneQuery.data!; if (!cloneQuery.data) throw new Error("unreachable: enabled guards data");
const { fs, dir } = cloneQuery.data;
return getCommitLog(fs, dir, ref); return getCommitLog(fs, dir, ref);
}, },
enabled: !!cloneQuery.data, enabled: !!cloneQuery.data,
@@ -80,7 +82,8 @@ export function useGitReadme(owner: string, repoName: string, ref: string) {
return useQuery({ return useQuery({
queryKey: ["git-readme", owner, repoName, ref], queryKey: ["git-readme", owner, repoName, ref],
queryFn: async () => { queryFn: async () => {
const { fs, dir } = cloneQuery.data!; if (!cloneQuery.data) throw new Error("unreachable: enabled guards data");
const { fs, dir } = cloneQuery.data;
return findReadme(fs, dir, ref); return findReadme(fs, dir, ref);
}, },
enabled: !!cloneQuery.data, enabled: !!cloneQuery.data,
@@ -100,7 +103,8 @@ export function useGitBlob(
return useQuery({ return useQuery({
queryKey: ["git-blob", owner, repoName, ref, filepath], queryKey: ["git-blob", owner, repoName, ref, filepath],
queryFn: async () => { queryFn: async () => {
const { fs, dir } = cloneQuery.data!; if (!cloneQuery.data) throw new Error("unreachable: enabled guards data");
const { fs, dir } = cloneQuery.data;
const oid = await resolveRef({ fs, dir, ref }); const oid = await resolveRef({ fs, dir, ref });
return readBlobView(fs, dir, oid, filepath); return readBlobView(fs, dir, oid, filepath);
}, },
@@ -128,7 +132,8 @@ export function useGitHtmlDoc(
return useQuery({ return useQuery({
queryKey: ["git-html-doc", owner, repoName, ref, filepath], queryKey: ["git-html-doc", owner, repoName, ref, filepath],
queryFn: async () => { queryFn: async () => {
const { fs, dir } = cloneQuery.data!; if (!cloneQuery.data) throw new Error("unreachable: enabled guards data");
const { fs, dir } = cloneQuery.data;
const oid = await resolveRef({ fs, dir, ref }); const oid = await resolveRef({ fs, dir, ref });
return resolveHtmlAssets(fs, dir, oid, filepath, html); return resolveHtmlAssets(fs, dir, oid, filepath, html);
}, },