Skip to content

Commit 44463f7

Browse files
authored
test(e2e): wait for app readiness before interactions (#65)
* test(e2e): wait for app readiness before interactions * test(e2e): exercise visible DM actions
1 parent 701505b commit 44463f7

3 files changed

Lines changed: 44 additions & 9 deletions

File tree

tests/e2e/app-ready.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { expect, type Page } from "@playwright/test";
2+
3+
export async function waitForAppReady(page: Page) {
4+
// SSR exposes controls before Svelte binds their handlers. Realtime only
5+
// connects after the client has mounted and started the selected workspace.
6+
await expect(page.locator('.shell[data-connected="true"]')).toBeVisible();
7+
}

tests/e2e/appearance.spec.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, test } from "@playwright/test";
2+
import { waitForAppReady } from "./app-ready";
23

34
// Appearance prefs are device-local: the settings modal writes localStorage
45
// and data attributes on <html>; base.css maps those to color-scheme and
@@ -7,9 +8,19 @@ import { expect, test } from "@playwright/test";
78

89
async function openAppearanceSettings(page: import("@playwright/test").Page) {
910
await page.goto("/app");
10-
await page.getByRole("button", { name: /account settings/i }).click();
11-
await page.getByRole("button", { name: "Appearance" }).click();
12-
await expect(page.getByRole("heading", { name: "Appearance" })).toBeVisible();
11+
await waitForAppReady(page);
12+
const accountSettings = page.getByRole("button", { name: /account settings/i });
13+
const modal = page.getByRole("dialog", { name: "Account settings" });
14+
const appearanceHeading = modal.getByRole("heading", { name: "Appearance" });
15+
await expect(async () => {
16+
if (await appearanceHeading.isVisible()) return;
17+
if (!(await modal.isVisible())) await accountSettings.click();
18+
await expect(modal.getByRole("heading", { name: "Profile settings" })).toBeVisible({
19+
timeout: 750,
20+
});
21+
await modal.getByRole("button", { name: "Appearance" }).click();
22+
await expect(appearanceHeading).toBeVisible({ timeout: 750 });
23+
}).toPass({ timeout: 5_000 });
1324
}
1425

1526
test("forced color mode applies instantly and survives reload", async ({ page }) => {

tests/e2e/chat.spec.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
openClawWorkspaceIdentifier,
1212
} from "../../apps/web/src/lib/bots";
1313
import { productAppURLForHost } from "../../apps/web/src/productLinks";
14+
import { waitForAppReady } from "./app-ready";
1415

1516
const serverURL = "http://127.0.0.1:18082";
1617
const execFileAsync = promisify(execFile);
@@ -259,6 +260,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
259260
}
260261

261262
await page.goto(`/app/${workspace.route_id}`);
263+
await waitForAppReady(page);
262264
const channelNames = (targetPage: Page) =>
263265
targetPage
264266
.locator("#sidebar-channels-list")
@@ -269,6 +271,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
269271

270272
const peer = await page.context().newPage();
271273
await peer.goto(`/app/${workspace.route_id}`);
274+
await waitForAppReady(peer);
272275
await expect.poll(() => channelNames(peer)).toEqual(names);
273276

274277
const source = page.getByRole("button", { name: `Move #${names[2]}` });
@@ -279,6 +282,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
279282
await expect.poll(() => channelNames(peer)).toEqual([names[2], names[0], names[1]]);
280283

281284
await page.reload();
285+
await waitForAppReady(page);
282286
await expect.poll(() => channelNames(page)).toEqual([names[2], names[0], names[1]]);
283287

284288
await page.getByRole("button", { name: "Channels", exact: true }).click();
@@ -290,6 +294,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
290294
await expect(page.getByRole("button", { name: `Move #${names[0]}` })).toHaveCount(0);
291295
await expect(page.getByRole("link", { name: `# ${names[0]}` })).toHaveCSS("flex-grow", "1");
292296
await page.reload();
297+
await waitForAppReady(page);
293298
await expect(page.getByRole("button", { name: "Channels", exact: true })).toHaveAttribute(
294299
"aria-expanded",
295300
"false",
@@ -310,6 +315,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
310315
});
311316
expect(addedResponse.ok()).toBe(true);
312317
await page.reload();
318+
await waitForAppReady(page);
313319
await expect.poll(() => channelNames(page)).toEqual([names[0], names[2], names[1], addedName]);
314320

315321
const secondWorkspaceResponse = await page.request.post("/api/workspaces", {
@@ -326,8 +332,10 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
326332
expect(response.ok()).toBe(true);
327333
}
328334
await page.goto(`/app/${secondWorkspace.route_id}`);
335+
await waitForAppReady(page);
329336
await expect.poll(() => channelNames(page)).toEqual([`aa-other-${suffix}`, `zz-other-${suffix}`]);
330337
await page.goto(`/app/${workspace.route_id}`);
338+
await waitForAppReady(page);
331339
await expect.poll(() => channelNames(page)).toEqual([names[0], names[2], names[1], addedName]);
332340

333341
const storageKey = await page.evaluate(() =>
@@ -336,13 +344,15 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
336344
expect(storageKey).toBeTruthy();
337345
await page.evaluate((key) => localStorage.setItem(key, "not-json"), storageKey!);
338346
await page.reload();
347+
await waitForAppReady(page);
339348
await expect.poll(() => channelNames(page)).toEqual([names[0], addedName, names[1], names[2]]);
340349

341350
await page.evaluate(({ key, value }) => localStorage.setItem(key, value), {
342351
key: storageKey!,
343352
value: "x".repeat(1_000_001),
344353
});
345354
await page.reload();
355+
await waitForAppReady(page);
346356
await expect.poll(() => channelNames(page)).toEqual([names[0], addedName, names[1], names[2]]);
347357

348358
await page.addInitScript(() => {
@@ -359,6 +369,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
359369
};
360370
});
361371
await page.reload();
372+
await waitForAppReady(page);
362373
await page.getByRole("button", { name: `Move #${names[2]}` }).focus();
363374
await page.keyboard.press("ArrowUp");
364375
await expect.poll(() => channelNames(page)).toEqual([names[0], addedName, names[2], names[1]]);
@@ -373,6 +384,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
373384
});
374385
const mobilePage = await mobileContext.newPage();
375386
await mobilePage.goto(`/app/${workspace.route_id}`);
387+
await waitForAppReady(mobilePage);
376388
await mobilePage.getByRole("button", { name: "Toggle navigation" }).click();
377389
await mobilePage.getByRole("button", { name: `Move #${names[1]}` }).click();
378390
await mobilePage
@@ -383,6 +395,7 @@ test("channels can be reordered accessibly and persist locally", async ({ page,
383395
.poll(() => channelNames(mobilePage))
384396
.toEqual([names[0], names[1], addedName, names[2]]);
385397
await mobilePage.reload();
398+
await waitForAppReady(mobilePage);
386399
await mobilePage.getByRole("button", { name: "Toggle navigation" }).click();
387400
await expect
388401
.poll(() => channelNames(mobilePage))
@@ -972,6 +985,7 @@ test("sends messages, searches, uploads, opens a thread, and creates a DM", asyn
972985
).trim();
973986

974987
await page.goto("/app");
988+
await waitForAppReady(page);
975989
await expect(page).toHaveURL(/\/app\/[^/]+\/[^/]+$/);
976990

977991
await page
@@ -1128,7 +1142,7 @@ test("closes direct messages without deleting history", async ({ page }) => {
11281142
workspaces: { id: string; route_id: string }[];
11291143
};
11301144
const workspace = workspaces.workspaces[0];
1131-
const name = `Close User ${Date.now()}`;
1145+
const name = `Close User ${randomUUID().replaceAll("-", "").slice(0, 12)}`;
11321146
const otherUserId = clickclack([
11331147
"admin",
11341148
"user",
@@ -1151,13 +1165,14 @@ test("closes direct messages without deleting history", async ({ page }) => {
11511165
};
11521166

11531167
await page.goto("/app");
1168+
await waitForAppReady(page);
11541169
const dmSection = page.locator(".nav-section", { hasText: "Direct messages" });
1155-
const dmLink = dmSection.getByRole("link", { name: new RegExp(name) });
1170+
const dmRow = dmSection.locator(".dm-row").filter({ hasText: name });
1171+
const dmLink = dmRow.getByRole("link", { name: new RegExp(name) });
11561172
const closeDirectMessage = async () => {
1157-
await dmSection
1158-
.getByRole("button", { name: `Direct message actions for ${name}` })
1159-
.click({ force: true });
1160-
await dmSection.getByRole("menuitem", { name: "Close direct message" }).click();
1173+
await dmRow.hover();
1174+
await dmRow.getByRole("button", { name: `Direct message actions for ${name}` }).click();
1175+
await dmRow.getByRole("menuitem", { name: "Close direct message" }).click();
11611176
};
11621177
await expect(dmLink).toBeVisible();
11631178
await closeDirectMessage();
@@ -1171,6 +1186,7 @@ test("closes direct messages without deleting history", async ({ page }) => {
11711186
const hiddenGet = await page.request.get(`/api/dms/${conversation.id}`);
11721187
expect(hiddenGet.ok()).toBe(true);
11731188
await page.goto(`/app/${workspace.route_id}/${conversation.route_id}`);
1189+
await waitForAppReady(page);
11741190
await expect(page.getByRole("heading", { name: new RegExp(name) })).toBeVisible();
11751191

11761192
await closeDirectMessage();
@@ -1182,6 +1198,7 @@ test("closes direct messages without deleting history", async ({ page }) => {
11821198
const reopenedBody = (await reopened.json()) as { conversation: { id: string } };
11831199
expect(reopenedBody.conversation.id).toBe(conversation.id);
11841200
await page.reload();
1201+
await waitForAppReady(page);
11851202
await expect(dmLink).toBeVisible();
11861203

11871204
await closeDirectMessage();

0 commit comments

Comments
 (0)