1
9import { expect, type Page, type Locator } from "@playwright/test";
10
11
15export async function waitForSelectorByText(
16 page: Page,
17 testId: string,
18 text: string | RegExp,
19 timeoutMs = 15_000,
20): Promise<Locator> {
21 const locator = page
22 .getByTestId(testId)
23 .filter({ hasText: text });
24 await expect(locator.first()).toBeVisible({ timeout: timeoutMs });
25 return locator.first();
26}
27
28
35export async function waitForNetworkIdle(
36 page: Page,
37 _quietMs = 500,
38 timeoutMs = 15_000,
39): Promise<void> {
40 await page
41 .waitForLoadState("networkidle", { timeout: timeoutMs })
42 .catch((err) => {
43 throw new Error(
44 `waitForNetworkIdle: not idle within ${timeoutMs}ms ` +
45 `(a live WebSocket keeps the socket busy — wait on an explicit UI ` +
46 `state instead). Cause: ${String(err)}`,
47 );
48 });
49}
50
51
54export async function waitForCondition<T>(
55 page: Page,
56 fn: () => Promise<T> | T,
57 opts: { timeoutMs?: number; intervalMs?: number; message?: string } = {},
58): Promise<T> {
59 const timeoutMs = opts.timeoutMs ?? 15_000;
60 const intervalMs = opts.intervalMs ?? 250;
61 const deadline = Date.now() + timeoutMs;
62
63 while (true) {
64 const result = await fn();
65 if (result) return result;
66 if (Date.now() >= deadline) {
67 throw new Error(
68 `waitForCondition: timed out after ${timeoutMs}ms` +
69 (opts.message ? ` — ${opts.message}` : ""),
70 );
71 }
72 await page.waitForTimeout(intervalMs);
73 }
74}
75