diff --git a/README.md b/README.md index 471ee6c..b58cf0c 100644 --- a/README.md +++ b/README.md @@ -213,10 +213,10 @@ Recording begins at browser-context creation. By default, `videoMode` trims setu ```ts const video = videoMode(); // trimStart: "auto" is the default -await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); +await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); // The rendered video starts here, including this click's auto-wait. -await plugged.getByRole("button", { name: "Create report" }).click(); +await page.getByRole("button", { name: "Create report" }).click(); // start when a known "ready" element first becomes visible (falls back to // blank detection if it never appears): diff --git a/demo-video/src/snippets.ts b/demo-video/src/snippets.ts index 6629c4e..3a8ae0b 100644 --- a/demo-video/src/snippets.ts +++ b/demo-video/src/snippets.ts @@ -38,11 +38,11 @@ export const helperLines = (appearFrom: number, focusAt: number): CodeLine[] => { text: `import { addPlugins, spinnerWaiter } from "middlewright";` }, { text: `` }, { text: `export const test = base.extend({` }, - { text: ` page: async ({ page }, use, testInfo) => {` }, - { text: ` await using plugged = await addPlugins({` }, - { text: ` page, testInfo, plugins: [spinnerWaiter()],`, kind: "focus" }, + { text: ` page: async ({ page: basePage }, use, testInfo) => {` }, + { text: ` await using page = await addPlugins({` }, + { text: ` page: basePage, testInfo, plugins: [spinnerWaiter()],`, kind: "focus" }, { text: ` });` }, - { text: ` await use(plugged);` }, + { text: ` await use(page);` }, { text: ` },` }, { text: `});` }, ]; diff --git a/spec/debug-mode.spec.ts b/spec/debug-mode.spec.ts index c5dbf58..6f512e6 100644 --- a/spec/debug-mode.spec.ts +++ b/spec/debug-mode.spec.ts @@ -10,12 +10,12 @@ import { videoMode, } from "../src/index.ts"; -test("action middleware plugins are inert when PWDEBUG is set", async ({ page }, testInfo) => { +test("action middleware plugins are inert when PWDEBUG is set", async ({ page: basePage }, testInfo) => { using _debug = withPwdebug(); let recoveryCalls = 0; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ llmRecover({ @@ -29,7 +29,7 @@ test("action middleware plugins are inert when PWDEBUG is set", async ({ page }, spinnerWaiter({ spinnerTimeout: 3001 }), ], }); - await plugged.setContent(` + await page.setContent(`
hydrating forever
Exploded visibly
@@ -37,7 +37,7 @@ test("action middleware plugins are inert when PWDEBUG is set", async ({ page }, `); const start = Date.now(); - const error = await plugged + const error = await page .getByRole("button", { name: "Submit approval" }) .click({ timeout: 100 }) .catch((e: Error) => e); @@ -50,32 +50,32 @@ test("action middleware plugins are inert when PWDEBUG is set", async ({ page }, expect(recoveryCalls).toBe(0); }); -test("videoMode controls are inert when PWDEBUG is set", async ({ page }, testInfo) => { +test("videoMode controls are inert when PWDEBUG is set", async ({ page: basePage }, testInfo) => { using _debug = withPwdebug(); - const plugged = await addPlugins({ - page, + const page = await addPlugins({ + page: basePage, testInfo, plugins: [videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 20 } })], }); - await plugged.setContent(``); + await page.setContent(``); - plugged.videoMode.setStartTime(); - await plugged.videoMode.deadAir(async () => { - await plugged.waitForTimeout(20); + page.videoMode.setStartTime(); + await page.videoMode.deadAir(async () => { + await page.waitForTimeout(20); }); - await plugged.getByRole("button", { name: "press" }).click(); - plugged.videoMode.setEndTime(); + await page.getByRole("button", { name: "press" }).click(); + page.videoMode.setEndTime(); - await expect(plugged.videoMode.metadata()).resolves.toMatchObject({ + await expect(page.videoMode.metadata()).resolves.toMatchObject({ deadAir: [], highlights: [], outputs: {}, sourceRange: {}, }); - expect(plugged.videoMode.getVideoTimestamp()).toBe(0); + expect(page.videoMode.getVideoTimestamp()).toBe(0); - await plugged[Symbol.asyncDispose](); + await page[Symbol.asyncDispose](); expect(existsSync(join(testInfo.outputDir, "video-mode.json"))).toBe(false); }); diff --git a/spec/hydration-waiter.spec.ts b/spec/hydration-waiter.spec.ts index 8024740..931f1b8 100644 --- a/spec/hydration-waiter.spec.ts +++ b/spec/hydration-waiter.spec.ts @@ -1,30 +1,30 @@ import { test, expect } from "@playwright/test"; import { addPlugins, hydrationWaiter } from "../src/index.ts"; -test("waits for hydration before clicking", async ({ page }, testInfo) => { - await using plugged = await addPlugins({ - page, +test("waits for hydration before clicking", async ({ page: basePage }, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [hydrationWaiter()], }); - await plugged.setContent(getTestPageHtml(1500)); + await page.setContent(getTestPageHtml(1500)); // The click handler only exists after "hydration" (1.5s in). Without the // plugin, this click would land on a dead button. - await plugged.locator("#cta").click(); - await plugged.locator("#result", { hasText: "done" }).waitFor(); + await page.locator("#cta").click(); + await page.locator("#result", { hasText: "done" }).waitFor(); }); -test("without the plugin, clicking a dead button does nothing", async ({ page }, testInfo) => { - await using plugged = await addPlugins({ - page, +test("without the plugin, clicking a dead button does nothing", async ({ page: basePage }, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [hydrationWaiter({ disabled: true })], }); - await plugged.setContent(getTestPageHtml(1500)); + await page.setContent(getTestPageHtml(1500)); - await plugged.locator("#cta").click(); - const error = await plugged + await page.locator("#cta").click(); + const error = await page .locator("#result", { hasText: "done" }) .waitFor() .catch((e: Error) => e); @@ -33,16 +33,16 @@ test("without the plugin, clicking a dead button does nothing", async ({ page }, expect((error as Error).message).toMatch(/Timeout .* exceeded/); }); -test("custom selector", async ({ page }, testInfo) => { - await using plugged = await addPlugins({ - page, +test("custom selector", async ({ page: basePage }, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [hydrationWaiter({ selector: ".app-loading" })], }); - await plugged.setContent(getTestPageHtml(1500, "app-loading")); + await page.setContent(getTestPageHtml(1500, "app-loading")); - await plugged.locator("#cta").click(); - await plugged.locator("#result", { hasText: "done" }).waitFor(); + await page.locator("#cta").click(); + await page.locator("#result", { hasText: "done" }).waitFor(); }); function getTestPageHtml(hydrationDelayMs: number, markerClass?: string) { diff --git a/spec/llm-recover.spec.ts b/spec/llm-recover.spec.ts index 1ed163d..67f4bc1 100644 --- a/spec/llm-recover.spec.ts +++ b/spec/llm-recover.spec.ts @@ -6,21 +6,21 @@ import { addPlugins, llmRecover, type LlmRecoverOptions } from "../src/index.ts" // --- Provider-injected tests (no API key needed, always run) --- test("recovers using the injected provider and records a soft failure", async ({ - page, + page: basePage, }, testInfo) => { - const { plugged, assertions } = await plug(page, testInfo, { + const { page, assertions } = await plug(basePage, testInfo, { requestRecoveryCode: async () => ({ code: `async function recover({ page }) { await page.getByText("Create your profile").click(); }`, description: "stale copy: the button says 'Create your profile'", }), }); - await plugged.setContent(getProfilePageHtml()); + await page.setContent(getProfilePageHtml()); // Stale copy — the button actually says "Create your profile" - await plugged.getByText("Create profile").click(); + await page.getByText("Create profile").click(); // The injected recovery code found and clicked the real button - await plugged.getByText("profile created").waitFor(); + await page.getByText("profile created").waitFor(); expect(assertions).toHaveLength(1); expect(assertions[0]).toMatch(/click failed and was recovered by LLM/); @@ -28,10 +28,10 @@ test("recovers using the injected provider and records a soft failure", async ({ }); test("retries with attempt history, then rethrows with a summary", async ({ - page, + page: basePage, }, testInfo) => { const historySizes: number[] = []; - const { plugged } = await plug(page, testInfo, { + const { page } = await plug(basePage, testInfo, { maxAttempts: 2, requestRecoveryCode: async (_context, attemptHistory) => { historySizes.push(attemptHistory.length); @@ -41,9 +41,9 @@ test("retries with attempt history, then rethrows with a summary", async ({ }; }, }); - await plugged.setContent(`
no buttons here
`); + await page.setContent(`
no buttons here
`); - const error = await plugged + const error = await page .getByText("Create profile") .click() .catch((e: Error) => e); @@ -62,16 +62,16 @@ test("retries with attempt history, then rethrows with a summary", async ({ expect(artifact).toMatchObject({ recovered: false, method: "click" }); }); -test("provider returning no code rethrows the original error", async ({ page }, testInfo) => { - const { plugged, assertions } = await plug(page, testInfo, { +test("provider returning no code rethrows the original error", async ({ page: basePage }, testInfo) => { + const { page, assertions } = await plug(basePage, testInfo, { requestRecoveryCode: async () => ({ code: null, description: "Not recoverable: this page has no profile creation at all.", }), }); - await plugged.setContent(`
no buttons here
`); + await page.setContent(`
no buttons here
`); - const error = await plugged + const error = await page .getByText("Create profile") .click() .catch((e: Error) => e); @@ -87,15 +87,15 @@ test("provider returning no code rethrows the original error", async ({ page }, const apiTest = process.env.LLM_RECOVER ? test : test.skip; -apiTest("recovers from out-of-date copy", async ({ page }, testInfo) => { - const { plugged, assertions } = await plug(page, testInfo, {}); - await plugged.setContent(getProfilePageHtml()); +apiTest("recovers from out-of-date copy", async ({ page: basePage }, testInfo) => { + const { page, assertions } = await plug(basePage, testInfo, {}); + await page.setContent(getProfilePageHtml()); // The test uses stale copy — button actually says "Create your profile" - await plugged.getByText("Create profile").click(); + await page.getByText("Create profile").click(); // Recovery should have found and clicked the real button - await plugged.getByText("profile created").waitFor(); + await page.getByText("profile created").waitFor(); expect(assertions).toHaveLength(1); expect(assertions[0]).toMatch(/click failed and was recovered by LLM/); @@ -106,9 +106,9 @@ apiTest("recovers from out-of-date copy", async ({ page }, testInfo) => { expect(flat).toMatch(/Recovery code: await page\.getBy\w+\(.*'Create your profile'.*\)\.click\(\)/); }); -apiTest("recovers from timing issue by waiting", async ({ page }, testInfo) => { - const { plugged, assertions } = await plug(page, testInfo, {}); - await plugged.setContent(` +apiTest("recovers from timing issue by waiting", async ({ page: basePage }, testInfo) => { + const { page, assertions } = await plug(basePage, testInfo, {}); + await page.setContent(`

Welcome

You'll be able to create your profile in five seconds - hang tight

@@ -127,18 +127,18 @@ apiTest("recovers from timing issue by waiting", async ({ page }, testInfo) => { `); // Button doesn't exist yet — appears after 5s - await plugged.getByText("Create profile").click(); + await page.getByText("Create profile").click(); // Recovery should have waited and then clicked - await plugged.getByText("profile created").waitFor(); + await page.getByText("profile created").waitFor(); expect(assertions).toHaveLength(1); expect(assertions[0]).toMatch(/click failed and was recovered by LLM/); }); -apiTest("rethrows with context for genuine error", async ({ page }, testInfo) => { - const { plugged } = await plug(page, testInfo, {}); - await plugged.setContent(` +apiTest("rethrows with context for genuine error", async ({ page: basePage }, testInfo) => { + const { page } = await plug(basePage, testInfo, {}); + await page.setContent(`

Welcome

Creating profile not allowed for preview users

@@ -146,14 +146,14 @@ apiTest("rethrows with context for genuine error", async ({ page }, testInfo) => `); await expect(async () => { - await plugged.getByText("Create profile").click(); + await page.getByText("Create profile").click(); }).rejects.toThrow(/Not recoverable/i); }); // --- helpers --- /** Add the llm-recover plugin with a shimmed `expect.soft` that records instead of failing. */ -async function plug(page: Page, testInfo: TestInfo, options: LlmRecoverOptions) { +async function plug(basePage: Page, testInfo: TestInfo, options: LlmRecoverOptions) { const assertions: string[] = []; const mockExpectSoft = (actual: unknown, message: string) => { return { @@ -167,12 +167,12 @@ async function plug(page: Page, testInfo: TestInfo, options: LlmRecoverOptions) soft: mockExpectSoft, }) as typeof expect; - const plugged = await addPlugins({ - page, + const page = await addPlugins({ + page: basePage, testInfo, plugins: [llmRecover({ expect: shimmedExpect, ...options })], }); - return { plugged, assertions }; + return { page, assertions }; } function getProfilePageHtml() { diff --git a/spec/plugin-system.spec.ts b/spec/plugin-system.spec.ts index e7235fb..13e3885 100644 --- a/spec/plugin-system.spec.ts +++ b/spec/plugin-system.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test"; import { addPlugins, adjustError, type Plugin } from "../src/index.ts"; -test("middleware wraps actions in registration order", async ({ page }, testInfo) => { +test("middleware wraps actions in registration order", async ({ page: basePage }, testInfo) => { const calls: string[] = []; const tracer = (name: string): Plugin => ({ name, @@ -13,16 +13,15 @@ test("middleware wraps actions in registration order", async ({ page }, testInfo }, }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [tracer("outer"), tracer("inner")], }); - await plugged.setContent(``); + await page.setContent(``); - await plugged.locator("#name").fill("hello"); + await page.locator("#name").fill("hello"); - expect(await plugged.locator("#name").inputValue()).toBe("hello"); expect(calls).toEqual([ "outer:before:fill", "inner:before:fill", @@ -31,10 +30,10 @@ test("middleware wraps actions in registration order", async ({ page }, testInfo ]); }); -test("middleware can pass adjusted action args to later middleware", async ({ page }, testInfo) => { +test("middleware can pass adjusted action args to later middleware", async ({ page: basePage }, testInfo) => { let innerArgs: unknown[] = []; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ { @@ -50,18 +49,17 @@ test("middleware can pass adjusted action args to later middleware", async ({ pa }, ], }); - await plugged.setContent(``); + await page.setContent(``); - await plugged.locator("#name").fill("original"); + await page.locator("#name").fill("original"); expect(innerArgs).toEqual(["rewritten"]); - expect(await plugged.locator("#name").inputValue()).toBe("rewritten"); }); -test("falsy entries in the plugins array are skipped", async ({ page }, testInfo) => { +test("falsy entries in the plugins array are skipped", async ({ page: basePage }, testInfo) => { const calls: string[] = []; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ false, @@ -70,17 +68,17 @@ test("falsy entries in the plugins array are skipped", async ({ page }, testInfo { name: "real", middleware: async (ctx, next) => (calls.push(ctx.method), next()) }, ], }); - await plugged.setContent(``); + await page.setContent(``); - await plugged.locator("button").click(); + await page.locator("button").click(); expect(calls).toEqual(["click"]); }); -test("middleware receives testInfo", async ({ page }, testInfo) => { +test("middleware receives testInfo", async ({ page: basePage }, testInfo) => { let seenTitle: string | undefined; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ { @@ -92,17 +90,17 @@ test("middleware receives testInfo", async ({ page }, testInfo) => { }, ], }); - await plugged.setContent(``); + await page.setContent(``); - await plugged.locator("button").click(); + await page.locator("button").click(); expect(seenTitle).toBe("middleware receives testInfo"); }); -test("middleware receives action timing", async ({ page }, testInfo) => { +test("middleware receives action timing", async ({ page: basePage }, testInfo) => { let seenTiming: any; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ { @@ -114,9 +112,9 @@ test("middleware receives action timing", async ({ page }, testInfo) => { }, ], }); - await plugged.setContent(``); + await page.setContent(``); - await plugged.locator("button").click(); + await page.locator("button").click(); expect(seenTiming).toMatchObject({ actionStartedAt: expect.any(Number), @@ -132,7 +130,9 @@ test("middleware receives action timing", async ({ page }, testInfo) => { }); }); -test("plugins can expose typed controls on the plugged page", async ({ page }, testInfo) => { +test("plugins can expose typed controls on the plugin-enhanced page", async ({ + page: basePage, +}, testInfo) => { const helper = { name: "page-helper", pageExtension: ({ page, testInfo }) => ({ @@ -150,42 +150,41 @@ test("plugins can expose typed controls on the plugged page", async ({ page }, t }; }>; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [helper], }); - await plugged.pageHelper.renderMessage("hello from a page extension"); + await page.pageHelper.renderMessage("hello from a page extension"); - await expect(plugged.locator("main")).toContainText("hello from a page extension"); - expect(plugged.pageHelper.title()).toBe("plugins can expose typed controls on the plugged page"); + await basePage.waitForSelector('main:has-text("hello from a page extension")'); + expect(page.pageHelper.title()).toBe("plugins can expose typed controls on the plugin-enhanced page"); }); test("pages without plugins fall through to the original behavior", async ({ - page, + page: basePage, context, }, testInfo) => { // Adding plugins to one page patches the Locator prototype globally... - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [{ name: "noop", middleware: async (_ctx, next) => next() }], }); - await plugged.setContent(``); - await plugged.locator("input").fill("plugged"); + await page.setContent(``); + await page.locator("input").fill("enhanced"); // ...but a page that never had plugins added must still work. const plainPage = await context.newPage(); await plainPage.setContent(``); - await plainPage.locator("input").fill("unplugged"); - expect(await plainPage.locator("input").inputValue()).toBe("unplugged"); + await plainPage.locator("input").fill("plain"); }); -test("lifecycle events fire on addPlugins and on dispose", async ({ page }, testInfo) => { +test("lifecycle events fire on addPlugins and on dispose", async ({ page: basePage }, testInfo) => { const events: string[] = []; - const plugged = await addPlugins({ - page, + const page = await addPlugins({ + page: basePage, testInfo, plugins: [ { @@ -205,7 +204,7 @@ test("lifecycle events fire on addPlugins and on dispose", async ({ page }, test expect(events).toEqual(["beforeTest"]); - await plugged[Symbol.asyncDispose](); + await page[Symbol.asyncDispose](); expect(events).toEqual(["beforeTest", "afterTest", "cleanup"]); }); diff --git a/spec/screenshot.spec.ts b/spec/screenshot.spec.ts index c3b6eb7..24bac86 100644 --- a/spec/screenshot.spec.ts +++ b/spec/screenshot.spec.ts @@ -2,13 +2,13 @@ import { statSync } from "node:fs"; import { test, expect } from "@playwright/test"; import { addPlugins, screenshot } from "../src/index.ts"; -test("saves matching successful actions under readable locator names", async ({ page }, testInfo) => { +test("saves matching successful actions under readable locator names", async ({ page: basePage }, testInfo) => { using _environment = environmentVariable("PLAYWRIGHT_SCREENSHOT", "getByText;getByRole"); - await using plugged = await addPlugins({ page, testInfo, plugins: [screenshot()] }); - await plugged.setContent(`dynamic-project-slug`); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [screenshot()] }); + await page.setContent(`dynamic-project-slug`); - await plugged.getByRole("link", { name: "dynamic-project-slug" }).waitFor(); - await plugged.getByRole("link", { name: "dynamic-project-slug" }).waitFor(); + await page.getByRole("link", { name: "dynamic-project-slug" }).waitFor(); + await page.getByRole("link", { name: "dynamic-project-slug" }).waitFor(); expect(testInfo.attachments).toMatchObject([ { @@ -32,10 +32,14 @@ test("saves matching successful actions under readable locator names", async ({ test("does not overwrite a screenshot from another page in the same test", async ({ context, - page, + page: basePage, }, testInfo) => { using _environment = environmentVariable("PLAYWRIGHT_SCREENSHOT", "getByRole"); - await using firstPage = await addPlugins({ page, testInfo, plugins: [screenshot()] }); + await using firstPage = await addPlugins({ + page: basePage, + testInfo, + plugins: [screenshot()], + }); await using secondPage = await addPlugins({ page: await context.newPage(), testInfo, @@ -53,12 +57,12 @@ test("does not overwrite a screenshot from another page in the same test", async ]); }); -test("does not capture a failed matching action", async ({ page }, testInfo) => { +test("does not capture a failed matching action", async ({ page: basePage }, testInfo) => { using _environment = environmentVariable("PLAYWRIGHT_SCREENSHOT", "missing"); - await using plugged = await addPlugins({ page, testInfo, plugins: [screenshot()] }); - await plugged.setContent(`
Nothing matching the locator
`); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [screenshot()] }); + await page.setContent(`
Nothing matching the locator
`); - const error = await plugged + const error = await page .locator("#missing") .waitFor() .catch((caught: Error) => caught); @@ -67,13 +71,13 @@ test("does not capture a failed matching action", async ({ page }, testInfo) => expect(testInfo.attachments).toEqual([]); }); -test("is inert when PWDEBUG is set", async ({ page }, testInfo) => { +test("is inert when PWDEBUG is set", async ({ page: basePage }, testInfo) => { using _environment = environmentVariable("PLAYWRIGHT_SCREENSHOT", ".*"); using _debug = environmentVariable("PWDEBUG", "1"); - await using plugged = await addPlugins({ page, testInfo, plugins: [screenshot()] }); - await plugged.setContent(``); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [screenshot()] }); + await page.setContent(``); - await plugged.getByRole("button", { name: "Save" }).click(); + await page.getByRole("button", { name: "Save" }).click(); expect(testInfo.attachments).toEqual([]); }); diff --git a/spec/spinner-waiter.spec.ts b/spec/spinner-waiter.spec.ts index 269f713..7c4ea5e 100644 --- a/spec/spinner-waiter.spec.ts +++ b/spec/spinner-waiter.spec.ts @@ -2,13 +2,13 @@ import { test as base, expect } from "@playwright/test"; import { addPlugins, spinnerWaiter, type Plugin } from "../src/index.ts"; const test = base.extend<{ slowMutationTimeout: number }>({ - page: async ({ page }, use, testInfo) => { - await using _page = await addPlugins({ - page, + page: async ({ page: basePage }, use, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [spinnerWaiter()], }); - await _page.setContent(` + await page.setContent(` Spinner Waiter Test @@ -21,7 +21,7 @@ const test = base.extend<{ slowMutationTimeout: number }>({ `); - await use(_page); + await use(page); }, }); @@ -48,7 +48,7 @@ test("visible disabled button succeeds when there's a spinner", async ({ page }) await page.getByRole("button", { name: "Submit approval" }).click(); - await expect(page.locator("#result")).toContainText("approval submitted"); + await page.locator("#result", { hasText: "approval submitted" }).waitFor(); }); test("slow button fails without spinner waiter", async ({ page }) => { @@ -94,10 +94,9 @@ test("fails before a late spinner can make the no-spinner hint misleading", asyn expect(error).toBeInstanceOf(Error); expect(error?.message).toMatch(/If this is a slow operation.../); expect(elapsed).toBeLessThan(1500); // we don't tolerate the spinner taking a long time to appear - expect(await page.locator('[aria-label="Loading"]').isVisible()).toBe(false); }); -base("no-spinner fast fail still runs later middleware", async ({ page }, testInfo) => { +base("no-spinner fast fail still runs later middleware", async ({ page: basePage }, testInfo) => { const calls: string[] = []; const afterSpinner: Plugin = { name: "after-spinner", @@ -110,14 +109,14 @@ base("no-spinner fast fail still runs later middleware", async ({ page }, testIn } }, }; - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [spinnerWaiter(), afterSpinner], }); - await plugged.setContent(``); + await page.setContent(``); - const error = await plugged + const error = await page .getByRole("button", { name: "Submit approval" }) .click() .catch((e: Error) => e); diff --git a/spec/ui-error-reporter.spec.ts b/spec/ui-error-reporter.spec.ts index e31ce31..ff93c12 100644 --- a/spec/ui-error-reporter.spec.ts +++ b/spec/ui-error-reporter.spec.ts @@ -1,13 +1,13 @@ import { test, expect } from "@playwright/test"; import { addPlugins, uiErrorReporter } from "../src/index.ts"; -test("appends visible error UI to failing action errors", async ({ page }, testInfo) => { - await using plugged = await addPlugins({ - page, +test("appends visible error UI to failing action errors", async ({ page: basePage }, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [uiErrorReporter()], }); - await plugged.setContent(` + await page.setContent(`
`); - await plugged.locator("#save").click(); + await page.locator("#save").click(); // The save "failed" (error toast appeared), so this element never shows up - const error = await plugged + const error = await page .locator("#saved-indicator") .waitFor() .catch((e: Error) => e); @@ -32,15 +32,15 @@ test("appends visible error UI to failing action errors", async ({ page }, testI expect((error as Error).message).toMatch(/quota exceeded/); }); -test("leaves errors alone when no error UI is visible", async ({ page }, testInfo) => { - await using plugged = await addPlugins({ - page, +test("leaves errors alone when no error UI is visible", async ({ page: basePage }, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [uiErrorReporter()], }); - await plugged.setContent(`
nothing to see here
`); + await page.setContent(`
nothing to see here
`); - const error = await plugged + const error = await page .locator("#missing") .waitFor() .catch((e: Error) => e); diff --git a/spec/video-mode-auto-start.spec.ts b/spec/video-mode-auto-start.spec.ts index 2dbd8d5..7c3b986 100644 --- a/spec/video-mode-auto-start.spec.ts +++ b/spec/video-mode-auto-start.spec.ts @@ -36,40 +36,40 @@ const blankThenContent = (options: { blankMs: number; markerAtMs?: number }) => `; -test("starts at the first locator invocation by default", async ({ page }, testInfo) => { +test("starts at the first locator invocation by default", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: false }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setContent(` + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setContent(` `); - const firstLocatorBefore = plugged.videoMode.getVideoTimestamp(); - await plugged.locator("#ready").waitFor(); - const firstLocatorAfter = plugged.videoMode.getVideoTimestamp(); + const firstLocatorBefore = page.videoMode.getVideoTimestamp(); + await page.locator("#ready").waitFor(); + const firstLocatorAfter = page.videoMode.getVideoTimestamp(); const firstStart = (await video.metadata()).sourceRange.start; expect(firstLocatorAfter - firstLocatorBefore).toBeGreaterThan(400); expect(firstStart).toBeGreaterThanOrEqual(firstLocatorBefore); expect(firstStart).toBeLessThan(firstLocatorBefore + 100); - await plugged.waitForTimeout(100); - await plugged.locator("#next").click(); + await page.waitForTimeout(100); + await page.locator("#next").click(); expect((await video.metadata()).sourceRange).toMatchObject({ start: firstStart }); } }); -test("detects the blank startup lead-in when requested", async ({ page }, testInfo) => { +test("detects the blank startup lead-in when requested", async ({ page: basePage }, testInfo) => { const blankMs = 2000; const video = videoMode({ finalHold: 0, highlight: false, trimStart: "detect-blank" }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(blankThenContent({ blankMs })); - await plugged.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); - await plugged.waitForTimeout(800); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(blankThenContent({ blankMs })); + await page.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); + await page.waitForTimeout(800); } const metadata = await video.metadata(); @@ -93,20 +93,20 @@ test("detects the blank startup lead-in when requested", async ({ page }, testIn expect(renderedOpening.red).toBeGreaterThan(renderedOpening.blue + 60); }); -test("starts from a selector the moment it becomes visible", async ({ page }, testInfo) => { +test("starts from a selector the moment it becomes visible", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: false, trimStart: ["selector", "#marker"], }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 800, height: 600 }); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 800, height: 600 }); // marker (the "ready" signal) shows well before the busy content paints, so a // selector-driven start must land earlier than the pixel detector would. - await plugged.setContent(blankThenContent({ blankMs: 2800, markerAtMs: 1000 })); - await plugged.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); - await plugged.waitForTimeout(500); + await page.setContent(blankThenContent({ blankMs: 2800, markerAtMs: 1000 })); + await page.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); + await page.waitForTimeout(500); } const metadata = await video.metadata(); @@ -122,16 +122,16 @@ test("starts from a selector the moment it becomes visible", async ({ page }, te }); test('trimStart: "detect-blank" leaves a video that was never blank untrimmed', async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: false, trimStart: "detect-blank" }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 800, height: 600 }); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 800, height: 600 }); // content is on screen from the first frame — nothing to trim - await plugged.setContent(blankThenContent({ blankMs: 0 })); - await plugged.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); - await plugged.waitForTimeout(800); + await page.setContent(blankThenContent({ blankMs: 0 })); + await page.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); + await page.waitForTimeout(800); } const metadata = await video.metadata(); @@ -139,15 +139,15 @@ test('trimStart: "detect-blank" leaves a video that was never blank untrimmed', }); test('trimStart: "never" disables trimming even with a long blank lead-in', async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: false, trimStart: "never" }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(blankThenContent({ blankMs: 2000 })); - await plugged.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); - await plugged.waitForTimeout(500); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(blankThenContent({ blankMs: 2000 })); + await page.locator("#tl").waitFor({ state: "visible", timeout: 10_000 }); + await page.waitForTimeout(500); } const metadata = await video.metadata(); diff --git a/spec/video-mode-ffmpeg.spec.ts b/spec/video-mode-ffmpeg.spec.ts index 669860c..64c8d1d 100644 --- a/spec/video-mode-ffmpeg.spec.ts +++ b/spec/video-mode-ffmpeg.spec.ts @@ -13,14 +13,14 @@ const execFile = promisify(execFileCallback); test.use({ video: "on" }); test("renders a multi-navigation release flow without changing the live page", async ({ - page, + page: basePage, }, testInfo) => { const urls = [ "https://dashboard.middlewright.test/runs", "https://dashboard.middlewright.test/releases/2026.7", "https://dashboard.middlewright.test/reports/128?browser=chromium", ]; - await page.route("https://dashboard.middlewright.test/**", async (route) => { + await basePage.route("https://dashboard.middlewright.test/**", async (route) => { await route.fulfill({ body: releaseDemoPage(new URL(route.request().url()).pathname), contentType: "text/html", @@ -34,27 +34,27 @@ test("renders a multi-navigation release flow without changing the live page", a trimStart: "never", }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 960, height: 540 }); - - await plugged.goto(urls[0]); - await plugged.getByRole("textbox", { name: "Search releases" }).fill("2026.7"); - await plugged.getByRole("button", { name: "Ready only" }).click(); - await expect(plugged.getByText("Release 2026.8-beta")).toHaveCount(0); - await plugged.waitForTimeout(250); - - await plugged.goto(urls[1]); - await plugged.getByRole("button", { name: "Chromium" }).click(); - await expect(plugged.getByText("48 Chromium specs passed")).toBeVisible(); - await plugged.waitForTimeout(250); - - await plugged.goto(urls[2]); - await plugged.getByRole("button", { name: "Show slowest specs" }).click(); - await expect(plugged.getByRole("table")).toBeVisible(); - await plugged.waitForTimeout(400); - await expect( - plugged.locator("[data-middlewright-video-mode-address-bar]"), - ).toHaveCount(0); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 960, height: 540 }); + + await page.goto(urls[0]); + await page.getByRole("textbox", { name: "Search releases" }).fill("2026.7"); + await page.getByRole("button", { name: "Ready only" }).click(); + await basePage.waitForSelector('text="Release 2026.8-beta"', { state: "hidden" }); + await page.waitForTimeout(250); + + await page.goto(urls[1]); + await page.getByRole("button", { name: "Chromium" }).click(); + await basePage.waitForSelector('text="48 Chromium specs passed"'); + await page.waitForTimeout(250); + + await page.goto(urls[2]); + await page.getByRole("button", { name: "Show slowest specs" }).click(); + await basePage.waitForSelector("table"); + await page.waitForTimeout(400); + await basePage.waitForSelector("[data-middlewright-video-mode-address-bar]", { + state: "hidden", + }); } const paths = video.outputPaths(); @@ -80,10 +80,10 @@ test("renders a multi-navigation release flow without changing the live page", a }); test("reveals a goto destination progressively in the rendered address bar", async ({ - page, + page: basePage, }, testInfo) => { const url = "https://dashboard.middlewright.test/releases/2026.8?view=review"; - await page.route(url, async (route) => { + await basePage.route(url, async (route) => { await route.fulfill({ body: '
', contentType: "text/html", @@ -96,10 +96,10 @@ test("reveals a goto destination progressively in the rendered address bar", asy trimStart: "never", }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 800, height: 450 }); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 800, height: 450 }); - await plugged.goto(url); + await page.goto(url); } const frames = (await videoFrames(video.outputPaths().rendered, 25)).filter(hasAddressBar); @@ -119,10 +119,10 @@ test("reveals a goto destination progressively in the rendered address bar", asy }); test("keeps a long goto destination in a compact address field", async ({ - page, + page: basePage, }, testInfo) => { const url = `https://dashboard.middlewright.test/releases/${"a-very-long-path-segment/".repeat(12)}report?browser=chromium&view=review`; - await page.route("https://dashboard.middlewright.test/**", async (route) => { + await basePage.route("https://dashboard.middlewright.test/**", async (route) => { await route.fulfill({ body: '
', contentType: "text/html", @@ -135,10 +135,10 @@ test("keeps a long goto destination in a compact address field", async ({ trimStart: "never", }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 960, height: 540 }); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 960, height: 540 }); - await plugged.goto(url); + await page.goto(url); } const frames = (await videoFrames(video.outputPaths().rendered, 25)).filter(hasAddressBar); @@ -167,10 +167,10 @@ test("keeps a long goto destination in a compact address field", async ({ }); test("renders navigation before clicking a control at the top edge", async ({ - page, + page: basePage, }, testInfo) => { const url = "https://dashboard.middlewright.test/runs/128"; - await page.route(url, async (route) => { + await basePage.route(url, async (route) => { await route.fulfill({ body: topEdgeActionPage(), contentType: "text/html", @@ -184,13 +184,13 @@ test("renders navigation before clicking a control at the top edge", async ({ trimStart: "never", }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 960, height: 540 }); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 960, height: 540 }); - await plugged.goto(url); - await plugged.getByRole("button", { name: "Approve run" }).click(); - await expect(plugged.getByRole("status")).toHaveText("Run approved"); - await plugged.waitForTimeout(400); + await page.goto(url); + await page.getByRole("button", { name: "Approve run" }).click(); + await basePage.waitForSelector('[role="status"]:has-text("Run approved")'); + await page.waitForTimeout(400); } const paths = video.outputPaths(); @@ -232,10 +232,10 @@ test("renders navigation before clicking a control at the top edge", async ({ }); test("keeps a navigation caption visible throughout its address-bar hold", async ({ - page, + page: basePage, }, testInfo) => { const url = "https://dashboard.middlewright.test/runs/128"; - await page.route(url, async (route) => { + await basePage.route(url, async (route) => { await route.fulfill({ body: '
', contentType: "text/html", @@ -248,11 +248,11 @@ test("keeps a navigation caption visible throughout its address-bar hold", async trimStart: "never", }); { - await using plugged = await addPlugins({ page, testInfo, plugins: [video] }); - await plugged.setViewportSize({ width: 800, height: 450 }); + await using page = await addPlugins({ page: basePage, testInfo, plugins: [video] }); + await page.setViewportSize({ width: 800, height: 450 }); await test.step("Open run 128", async () => { - await plugged.goto(url); + await page.goto(url); }); } @@ -265,7 +265,7 @@ test("keeps a navigation caption visible throughout its address-bar hold", async }); test("turns meaningful Playwright steps into readable video captions", async ({ - page, + page: basePage, }, testInfo) => { const deadAirThresholdMs = 300; const finalHoldMs = 1200; @@ -277,13 +277,13 @@ test("turns meaningful Playwright steps into readable video captions", async ({ trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(`

Create your account

@@ -355,25 +355,19 @@ test("turns meaningful Playwright steps into readable video captions", async ({ `); await test.step("Enter account details", async () => { - await plugged.getByLabel("Work email").fill("ada@example.com"); - await plugged.getByRole("button", { name: "Continue" }).click(); - const planHeading = plugged.getByRole("heading", { name: "Choose a plan" }); - await planHeading.waitFor(); - await expect(planHeading).toBeVisible(); + await page.getByLabel("Work email").fill("ada@example.com"); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("heading", { name: "Choose a plan" }).waitFor(); }); await test.step("Choose the Pro plan", async () => { - await plugged.getByRole("button", { name: "Pro" }).click(); - await plugged.getByRole("button", { name: "Continue" }).click(); - const reviewHeading = plugged.getByRole("heading", { name: "Review subscription" }); - await reviewHeading.waitFor(); - await expect(reviewHeading).toBeVisible(); - await expect(plugged.locator("#summary")).toContainText("ada@example.com · Pro"); + await page.getByRole("button", { name: "Pro" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("heading", { name: "Review subscription" }).waitFor(); + await basePage.waitForSelector('#summary:has-text("ada@example.com · Pro")'); }); await test.step("Confirm the subscription", async () => { - await plugged.getByRole("button", { name: "Confirm subscription" }).click(); - const successHeading = plugged.getByRole("heading", { name: "Welcome to Pro" }); - await successHeading.waitFor(); - await expect(successHeading).toBeVisible(); + await page.getByRole("button", { name: "Confirm subscription" }).click(); + await page.getByRole("heading", { name: "Welcome to Pro" }).waitFor(); }); } @@ -438,7 +432,7 @@ test("turns meaningful Playwright steps into readable video captions", async ({ }); test("keeps captions aligned through trimming and dead-air compression", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ captions: "explicit", @@ -448,21 +442,21 @@ test("keeps captions aligned through trimming and dead-air compression", async ( trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(`
`); - await plugged.videoMode.caption("Process account data", async () => { - await plugged.waitForTimeout(100); - plugged.videoMode.setStartTime(); - await plugged.videoMode.deadAir(async () => { - await plugged.waitForTimeout(700); + await page.videoMode.caption("Process account data", async () => { + await page.waitForTimeout(100); + page.videoMode.setStartTime(); + await page.videoMode.deadAir(async () => { + await page.waitForTimeout(700); }); }); } @@ -494,10 +488,10 @@ test("keeps captions aligned through trimming and dead-air compression", async ( }); test("writes a rendered video with dead air sped up and highlights added in post", async ({ - page, + page: basePage, }, testInfo) => { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ spinnerWaiter(), @@ -508,8 +502,8 @@ test("writes a rendered video with dead air sped up and highlights added in post }), ], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(`
@@ -564,19 +558,18 @@ test("writes a rendered video with dead air sped up and highlights added in post `); - await plugged.getByText("Start import").click(); - await plugged.getByText("Review records").click(); - await plugged.getByText("Approve import").click(); - await plugged.videoMode.deadAir(async () => { + await page.getByText("Start import").click(); + await page.getByText("Review records").click(); + await page.getByText("Approve import").click(); + await page.videoMode.deadAir(async () => { await new Promise((resolve) => setTimeout(resolve, 1700)); }); - await plugged.getByText("Download receipt").click(); + await page.getByText("Download receipt").click(); - await plugged.getByText("Receipt ready").waitFor(); - await expect(plugged.getByText("Receipt ready")).toContainText("Receipt ready"); + await page.getByText("Receipt ready").waitFor(); }); -test("writes video-mode artifact files and report player", async ({ page }, testInfo) => { +test("writes video-mode artifact files and report player", async ({ page: basePage }, testInfo) => { const deadAirThresholdMs = 300; const finalHoldMs = 500; const highlightDurationMs = 600; @@ -586,12 +579,12 @@ test("writes video-mode artifact files and report player", async ({ page }, test highlight: { mode: "pointer", duration: highlightDurationMs }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(`
`); - await plugged.videoMode.deadAir(async () => { + await page.videoMode.deadAir(async () => { await new Promise((resolve) => setTimeout(resolve, 1200)); }); - await plugged.locator("#save").click(); - await expect(plugged.locator("#status")).toContainText("saved"); + await page.locator("#save").click(); + await basePage.waitForSelector('#status:has-text("saved")'); } const paths = video.outputPaths(); @@ -661,25 +654,25 @@ test("writes video-mode artifact files and report player", async ({ page }, test const playerUrl = new URL(pathToFileURL(paths.player).href); playerUrl.searchParams.set("active", "rendered"); playerUrl.searchParams.set("frame", "2"); - const playerPage = await page.context().newPage(); + const playerPage = await basePage.context().newPage(); await playerPage.goto(playerUrl.href); - await expect(playerPage.locator("#active")).toHaveText("Rendered video"); - await expect(playerPage.locator("#frame")).toHaveText("2"); + await playerPage.locator("#active", { hasText: "Rendered video" }).waitFor(); + await playerPage.locator("#frame", { hasText: "2" }).waitFor(); await playerPage.keyboard.press("ArrowRight"); - await expect(playerPage.locator("#frame")).toHaveText("3"); + await playerPage.locator("#frame", { hasText: "3" }).waitFor(); expect(new URL(playerPage.url()).searchParams.get("active")).toBe("rendered"); expect(new URL(playerPage.url()).searchParams.get("frame")).toBe("3"); await playerPage.close(); }); -test("keeps the page open for later afterTest hooks", async ({ page }, testInfo) => { +test("keeps the page open for later afterTest hooks", async ({ page: basePage }, testInfo) => { const afterTestEvents: string[] = []; const afterVideoMode = { name: "after-video-mode", testLifecycle: (emitter) => { return emitter.on("afterTest", async ({ page }) => { afterTestEvents.push(page.isClosed() ? "closed" : "open"); - await expect(page.locator("#after-test-hook-target")).toContainText("ready"); + await page.waitForSelector('#after-test-hook-target:has-text("ready")'); }); }, } satisfies Plugin; @@ -689,19 +682,19 @@ test("keeps the page open for later afterTest hooks", async ({ page }, testInfo) }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video, afterVideoMode], }); - await plugged.setContent(`
ready
`); + await page.setContent(`
ready
`); } expect(afterTestEvents).toEqual(["open"]); }); test("speeds dead air to the default threshold instead of cutting through it", async ({ - page, + page: basePage, }, testInfo) => { const deadAirThresholdMs = 300; const video = videoMode({ @@ -710,18 +703,18 @@ test("speeds dead air to the default threshold instead of cutting through it", a highlight: { mode: "pointer", duration: 0 }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 400, height: 300 }); - await plugged.setContent(` + await page.setViewportSize({ width: 400, height: 300 }); + await page.setContent(`
`); - await plugged.videoMode.deadAir(async () => { - await plugged.evaluate(() => { + await page.videoMode.deadAir(async () => { + await page.evaluate(() => { const box = document.querySelector("#progress") as HTMLElement; const startedAt = performance.now(); const duration = 1600; @@ -734,7 +727,7 @@ test("speeds dead air to the default threshold instead of cutting through it", a }; update(); }); - await plugged.waitForTimeout(1600); + await page.waitForTimeout(1600); }); } @@ -759,24 +752,24 @@ test("speeds dead air to the default threshold instead of cutting through it", a expect(middleColor.blue).toBeGreaterThan(80); }); -test("renders only the selected video source range", async ({ page }, testInfo) => { +test("renders only the selected video source range", async ({ page: basePage }, testInfo) => { const video = videoMode({ trimStart: "never", finalHold: 0, highlight: { mode: "pointer", duration: 0 }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent("
source range
"); + await page.setContent("
source range
"); - await plugged.waitForTimeout(700); - plugged.videoMode.setStartTime(); - await plugged.waitForTimeout(1100); - plugged.videoMode.setEndTime(); - await plugged.waitForTimeout(700); + await page.waitForTimeout(700); + page.videoMode.setStartTime(); + await page.waitForTimeout(1100); + page.videoMode.setEndTime(); + await page.waitForTimeout(700); } const metadata = await video.metadata(); @@ -794,7 +787,7 @@ test("renders only the selected video source range", async ({ page }, testInfo) expect(Math.abs(renderedDuration - expectedRenderedDuration)).toBeLessThan(700); }); -test("skips rendering an empty selected video source range", async ({ page }, testInfo) => { +test("skips rendering an empty selected video source range", async ({ page: basePage }, testInfo) => { const video = videoMode({ trimStart: "never", finalHold: 0, highlight: false, @@ -808,16 +801,16 @@ test("skips rendering an empty selected video source range", async ({ page }, te try { { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent("
empty source range
"); + await page.setContent("
empty source range
"); - plugged.videoMode.setStartTime(0); - plugged.videoMode.setEndTime(0); - await plugged.waitForTimeout(100); + page.videoMode.setStartTime(0); + page.videoMode.setEndTime(0); + await page.waitForTimeout(100); } const metadata = await video.metadata(); @@ -847,7 +840,7 @@ test("skips rendering an empty selected video source range", async ({ page }, te }); test("holds the pre-click state without flashing the completed action state first", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 900; const video = videoMode({ trimStart: "never", @@ -855,20 +848,20 @@ test("holds the pre-click state without flashing the completed action state firs highlight: { mode: "outline", duration: highlightDurationMs, style: "8px solid yellow" }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(`
`); - await plugged.waitForTimeout(300); - await plugged.locator("#target").click(); - await expect(plugged.locator("#target")).toHaveCSS("background-color", "rgb(255, 0, 0)"); await page.waitForTimeout(300); + await page.locator("#target").click(); + await basePage.waitForSelector('#target[style*="rgb(255, 0, 0)"]'); + await basePage.waitForTimeout(300); } const paths = video.outputPaths(); @@ -942,7 +935,7 @@ test("holds the pre-click state without flashing the completed action state firs }); test("renders an accepted confirm with a paused dialog and pointer click", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 900; const video = videoMode({ @@ -951,13 +944,13 @@ test("renders an accepted confirm with a paused dialog and pointer click", async trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` @@ -970,11 +963,11 @@ test("renders an accepted confirm with a paused dialog and pointer click", async `); - plugged.once("dialog", (dialog) => dialog.accept()); + page.once("dialog", (dialog) => dialog.accept()); - await plugged.locator("#discard").click(); + await page.locator("#discard").click(); - await plugged.getByText("Discarded!", { exact: true }).waitFor(); + await page.getByText("Discarded!", { exact: true }).waitFor(); } const paths = video.outputPaths(); @@ -1049,7 +1042,7 @@ test("renders an accepted confirm with a paused dialog and pointer click", async }); test("reveals accepted prompt text progressively in the rendered dialog", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1000; const video = videoMode({ @@ -1058,13 +1051,13 @@ test("reveals accepted prompt text progressively in the rendered dialog", async trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` @@ -1075,11 +1068,11 @@ test("reveals accepted prompt text progressively in the rendered dialog", async `); - plugged.once("dialog", (dialog) => dialog.accept("correct 👩🏽‍💻 battery staple")); + page.once("dialog", (dialog) => dialog.accept("correct 👩🏽‍💻 battery staple")); - await plugged.locator("#sign-in").click(); + await page.locator("#sign-in").click(); - await expect(plugged.locator("#result")).toHaveText("correct 👩🏽‍💻 battery staple"); + await basePage.waitForSelector('#result:has-text("correct 👩🏽‍💻 battery staple")'); } const paths = video.outputPaths(); @@ -1180,7 +1173,7 @@ test("reveals accepted prompt text progressively in the rendered dialog", async }); test("clears a Unicode prompt default before selecting an explicit empty response", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 700; const video = videoMode({ @@ -1189,13 +1182,13 @@ test("clears a Unicode prompt default before selecting an explicit empty respons trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` @@ -1209,11 +1202,11 @@ test("clears a Unicode prompt default before selecting an explicit empty respons `); - plugged.once("dialog", (dialog) => dialog.accept("")); + page.once("dialog", (dialog) => dialog.accept("")); - await plugged.locator("#rename").click(); + await page.locator("#rename").click(); - await expect(plugged.locator("#result")).toHaveText('""'); + await basePage.waitForSelector('#result:has-text(\'""\')'); } const paths = video.outputPaths(); @@ -1281,7 +1274,7 @@ test("clears a Unicode prompt default before selecting an explicit empty respons }); test("uses natural post-dialog footage without adding a synthetic hold", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ finalHold: 0, @@ -1289,12 +1282,12 @@ test("uses natural post-dialog footage without adding a synthetic hold", async ( trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - plugged.once("dialog", (dialog) => dialog.accept()); + page.once("dialog", (dialog) => dialog.accept()); - await plugged.locator("#continue").click(); - await plugged.getByText("Processing", { exact: true }).waitFor(); - await plugged.waitForTimeout(1_100); + await page.locator("#continue").click(); + await page.getByText("Processing", { exact: true }).waitFor(); + await page.waitForTimeout(1_100); } const paths = video.outputPaths(); @@ -1329,7 +1322,7 @@ test("uses natural post-dialog footage without adding a synthetic hold", async ( }); test("uses the default final hold without leaving the pointer visible", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 700; const video = videoMode({ @@ -1337,19 +1330,19 @@ test("uses the default final hold without leaving the pointer visible", async ({ highlight: { mode: "pointer", duration: highlightDurationMs }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(`
`); - await plugged.locator("#target").click(); - await expect(plugged.locator("#target")).toHaveClass("clicked"); - await page.waitForTimeout(300); + await page.locator("#target").click(); + await basePage.waitForSelector("#target.clicked"); + await basePage.waitForTimeout(300); } const paths = video.outputPaths(); @@ -1390,7 +1383,7 @@ test("uses the default final hold without leaving the pointer visible", async ({ }); test("points at a visible result after waitFor without delaying the test", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 700; const video = videoMode({ trimStart: "never", @@ -1398,13 +1391,13 @@ test("points at a visible result after waitFor without delaying the test", async highlight: { mode: "pointer", duration: highlightDurationMs }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` `); - await plugged.waitForTimeout(1_000); - await plugged.getByRole("button", { name: "Sign in" }).click(); - await plugged.waitForTimeout(2_700); - await plugged.getByLabel("Title").fill("Check the demo pacing"); + await page.waitForTimeout(1_000); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForTimeout(2_700); + await page.getByLabel("Title").fill("Check the demo pacing"); } const metadata = await video.metadata(); @@ -1801,7 +1791,7 @@ test("does not flash a completed fill before its synthetic reveal", async ({ }); test("reveals complete glyphs instead of slicing through the next character", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1000; const video = videoMode({ @@ -1811,23 +1801,22 @@ test("reveals complete glyphs instead of slicing through the next character", as trimStart: ["selector", 'input[aria-label="Code"]'], }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` `); - await plugged.getByLabel("Code").waitFor(); + await page.getByLabel("Code").waitFor(); - await plugged.videoMode.caption("Reveal complete glyphs", async () => { - await plugged.getByLabel("Code").fill("A @ B"); - await expect(plugged.getByLabel("Code")).toHaveValue("A @ B"); + await page.videoMode.caption("Reveal complete glyphs", async () => { + await page.getByLabel("Code").fill("A @ B"); }); } @@ -1879,7 +1868,7 @@ test("reveals complete glyphs instead of slicing through the next character", as }); test("moves to the field and switches to the text cursor before revealing", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1200; const video = videoMode({ @@ -1890,23 +1879,22 @@ test("moves to the field and switches to the text cursor before revealing", asyn trimStart: "never", }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` `); - await plugged.getByLabel("Name").waitFor(); + await page.getByLabel("Name").waitFor(); - await plugged.videoMode.caption("Move, switch cursor, then reveal", async () => { - await plugged.getByLabel("Name").fill("Ada Lovelace"); - await expect(plugged.getByLabel("Name")).toHaveValue("Ada Lovelace"); + await page.videoMode.caption("Move, switch cursor, then reveal", async () => { + await page.getByLabel("Name").fill("Ada Lovelace"); }); } @@ -1959,7 +1947,7 @@ test("moves to the field and switches to the text cursor before revealing", asyn }); test("preserves gradient field pixels while revealing the filled text", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1000; const video = videoMode({ @@ -1969,23 +1957,22 @@ test("preserves gradient field pixels while revealing the filled text", async ({ trimStart: ["selector", 'input[aria-label="Gradient"]'], }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` `); - await plugged.getByLabel("Gradient").waitFor(); + await page.getByLabel("Gradient").waitFor(); - await plugged.videoMode.caption("Preserve gradient pixels", async () => { - await plugged.getByLabel("Gradient").fill("Ada"); - await expect(plugged.getByLabel("Gradient")).toHaveValue("Ada"); + await page.videoMode.caption("Preserve gradient pixels", async () => { + await page.getByLabel("Gradient").fill("Ada"); }); } @@ -2029,7 +2016,7 @@ test("preserves gradient field pixels while revealing the filled text", async ({ ).toBeGreaterThan(300); }); -test("reveals a stable single-line textarea fill", async ({ page }, testInfo) => { +test("reveals a stable single-line textarea fill", async ({ page: basePage }, testInfo) => { const highlightDurationMs = 1600; const video = videoMode({ captions: "explicit", @@ -2038,13 +2025,13 @@ test("reveals a stable single-line textarea fill", async ({ page }, testInfo) => trimStart: ["selector", 'textarea[aria-label="Notes"]'], }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` @@ -2062,17 +2049,12 @@ test("reveals a stable single-line textarea fill", async ({ page }, testInfo) => }); `); - await plugged.getByLabel("Notes").waitFor(); - - await plugged.videoMode.caption("Replace the placeholder", async () => { - await plugged.getByLabel("Notes").click(); - await expect(plugged.getByLabel("Notes")).toBeFocused(); - await plugged.getByLabel("Notes").fill("Ada notes"); - await expect(plugged.getByLabel("Notes")).toHaveValue("Ada notes"); - await expect(plugged.locator("body")).toHaveAttribute( - "data-seen-values", - JSON.stringify(["Ada notes"]), - ); + await page.getByLabel("Notes").waitFor(); + + await page.videoMode.caption("Replace the placeholder", async () => { + await page.getByLabel("Notes").click(); + await page.getByLabel("Notes").fill("Ada notes"); + await basePage.waitForSelector(`body[data-seen-values='["Ada notes"]']`); }); } @@ -2157,7 +2139,7 @@ test("reveals a stable single-line textarea fill", async ({ page }, testInfo) => }); test("reveals a scrolling textarea one visible line at a time", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1600; const video = videoMode({ @@ -2167,13 +2149,13 @@ test("reveals a scrolling textarea one visible line at a time", async ({ trimStart: ["selector", 'textarea[aria-label="Log"]'], }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` @@ -2183,18 +2165,15 @@ test("reveals a scrolling textarea one visible line at a time", async ({ style="position: absolute; box-sizing: border-box; left: 220px; top: 120px; width: 360px; height: 136px; border: 0; padding: 14px; resize: none; background: white; color: black; font: 30px/36px monospace" > `); - await plugged.getByLabel("Log").waitFor(); + await page.getByLabel("Log").waitFor(); - await plugged.videoMode.caption("Reveal the visible scrolled lines", async () => { - await plugged + await page.videoMode.caption("Reveal the visible scrolled lines", async () => { + await page .getByLabel("Log") .fill("first line\nsecond line\nthird line\nfourth line\nfifth line"); - await expect(plugged.getByLabel("Log")).toHaveValue( - "first line\nsecond line\nthird line\nfourth line\nfifth line", - ); await expect .poll(() => - plugged + page .getByLabel("Log") .evaluate((textarea) => textarea.scrollHeight > textarea.clientHeight), ) @@ -2280,7 +2259,7 @@ test("reveals a scrolling textarea one visible line at a time", async ({ }); test("reveals the final visible portion of a horizontally scrolling input", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1600; const video = videoMode({ @@ -2290,13 +2269,13 @@ test("reveals the final visible portion of a horizontally scrolling input", asyn trimStart: ["selector", 'input[aria-label="Reference"]'], }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` @@ -2306,17 +2285,14 @@ test("reveals the final visible portion of a horizontally scrolling input", asyn style="position: absolute; box-sizing: border-box; left: 220px; top: 160px; width: 360px; height: 80px; padding: 14px; background: white; color: black; font: 30px monospace" /> `); - await plugged.getByLabel("Reference").waitFor(); + await page.getByLabel("Reference").waitFor(); - await plugged.videoMode.caption("Reveal the visible reference suffix", async () => { - await plugged + await page.videoMode.caption("Reveal the visible reference suffix", async () => { + await page .getByLabel("Reference") .fill("prefix-that-scrolls-out-of-view-visible-reference-end"); - await expect(plugged.getByLabel("Reference")).toHaveValue( - "prefix-that-scrolls-out-of-view-visible-reference-end", - ); await expect - .poll(() => plugged.getByLabel("Reference").evaluate((input) => input.scrollLeft)) + .poll(() => page.getByLabel("Reference").evaluate((input) => input.scrollLeft)) .toBeGreaterThan(0); }); } @@ -2378,7 +2354,7 @@ test("reveals the final visible portion of a horizontally scrolling input", asyn }); test("reveals an expanding textarea one line at a time at its final geometry", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1600; const video = videoMode({ @@ -2389,13 +2365,13 @@ test("reveals an expanding textarea one line at a time at its final geometry", a }); let initialHeight = 0; { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(` @@ -2412,18 +2388,15 @@ test("reveals an expanding textarea one line at a time at its final geometry", a }); `); - await plugged.getByLabel("Summary").waitFor(); - initialHeight = (await plugged.getByLabel("Summary").boundingBox())!.height; + await page.getByLabel("Summary").waitFor(); + initialHeight = (await page.getByLabel("Summary").boundingBox())!.height; - await plugged.videoMode.caption("Reveal at the expanded size", async () => { - await plugged + await page.videoMode.caption("Reveal at the expanded size", async () => { + await page .getByLabel("Summary") .fill("This textarea grows to fit a longer summary without scrolling."); - await expect(plugged.getByLabel("Summary")).toHaveValue( - "This textarea grows to fit a longer summary without scrolling.", - ); await expect - .poll(async () => (await plugged.getByLabel("Summary").boundingBox())!.height) + .poll(async () => (await page.getByLabel("Summary").boundingBox())!.height) .toBeGreaterThan(initialHeight); }); } @@ -2551,7 +2524,7 @@ test("reveals an expanding textarea one line at a time at its final geometry", a }); test("uses a normal pointer tail after text cursor holds", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1000; const video = videoMode({ trimStart: "never", @@ -2559,21 +2532,19 @@ test("uses a normal pointer tail after text cursor holds", async ({ highlight: { mode: "pointer", duration: highlightDurationMs }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` `); - await plugged.locator("#name").fill("Ada"); - await expect(plugged.locator("#name")).toHaveValue("Ada"); - await plugged.locator("#notes").type("notes"); - await expect(plugged.locator("#notes")).toHaveValue("notes"); + await page.locator("#name").fill("Ada"); + await page.locator("#notes").type("notes"); } const paths = video.outputPaths(); @@ -2618,7 +2589,7 @@ test("uses a normal pointer tail after text cursor holds", async ({ }); test("does not replay action frames when a hold overlaps the next highlight", async ({ - page, + page: basePage, }, testInfo) => { const highlightDurationMs = 1000; const video = videoMode({ trimStart: "never", @@ -2626,13 +2597,13 @@ test("does not replay action frames when a hold overlaps the next highlight", as highlight: { mode: "pointer", duration: highlightDurationMs }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` @@ -2654,12 +2625,11 @@ test("does not replay action frames when a hold overlaps the next highlight", as `); - await plugged.locator("#name").fill("Ada"); - await expect(plugged.locator("body")).toHaveAttribute("data-transient-seen", "true"); - await expect(plugged.locator("body")).toHaveAttribute("data-phase", "stable"); - await plugged.locator("#run").click(); - await expect(plugged.locator("body")).toHaveAttribute("data-clicked", "true"); - await plugged.waitForTimeout(100); + await page.locator("#name").fill("Ada"); + await basePage.waitForSelector('body[data-transient-seen="true"][data-phase="stable"]'); + await page.locator("#run").click(); + await basePage.waitForSelector('body[data-clicked="true"]'); + await page.waitForTimeout(100); } const paths = video.outputPaths(); @@ -2689,7 +2659,7 @@ test("does not replay action frames when a hold overlaps the next highlight", as }); test("does not calibrate against an earlier occurrence of the final page state", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ trimStart: "never", @@ -2697,13 +2667,13 @@ test("does not calibrate against an earlier occurrence of the final page state", highlight: { mode: "pointer", duration: 600 }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` @@ -2724,11 +2694,11 @@ test("does not calibrate against an earlier occurrence of the final page state", `); - await plugged.waitForTimeout(1200); - await plugged.locator("#open").click(); - await plugged.getByText("Dialog ready").waitFor(); - await plugged.waitForTimeout(2000); - await plugged.locator("#close").click(); + await page.waitForTimeout(1200); + await page.locator("#open").click(); + await page.getByText("Dialog ready").waitFor(); + await page.waitForTimeout(2000); + await page.locator("#close").click(); } const frames = await videoFrames(video.outputPaths().rendered, 25); @@ -2744,7 +2714,7 @@ test("does not calibrate against an earlier occurrence of the final page state", }); test("does not linger on the unhighlighted post-wait state before a following highlight", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ trimStart: "never", deadAirThreshold: 300, @@ -2752,8 +2722,8 @@ test("does not linger on the unhighlighted post-wait state before a following hi highlight: { mode: "outline", duration: 600, style: "10px solid yellow" }, }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [ spinnerWaiter({ @@ -2763,8 +2733,8 @@ test("does not linger on the unhighlighted post-wait state before a following hi video, ], }); - await plugged.setViewportSize({ width: 800, height: 600 }); - await plugged.setContent(` + await page.setViewportSize({ width: 800, height: 600 }); + await page.setContent(` @@ -2788,10 +2758,10 @@ test("does not linger on the unhighlighted post-wait state before a following hi `); - await plugged.locator("#start").click(); - await plugged.locator("#next").click(); - await expect(plugged.locator("#done")).toContainText("done"); - await page.waitForTimeout(300); + await page.locator("#start").click(); + await page.locator("#next").click(); + await basePage.waitForSelector('#done:has-text("done")'); + await basePage.waitForTimeout(300); } const paths = video.outputPaths(); diff --git a/spec/video-mode-start-behaviors.spec.ts b/spec/video-mode-start-behaviors.spec.ts index 7a98271..c1b5153 100644 --- a/spec/video-mode-start-behaviors.spec.ts +++ b/spec/video-mode-start-behaviors.spec.ts @@ -3,12 +3,12 @@ import { addPlugins, videoMode } from "../src/index.ts"; test.use({ video: "on" }); -test("default starts at the first locator invocation", async ({ page }, testInfo) => { +test("default starts at the first locator invocation", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 700, highlight: false }); const metadata = await recordStartTimeline({ manualStartAtMs: false, - page, + basePage, testInfo, video, }); @@ -17,12 +17,12 @@ test("default starts at the first locator invocation", async ({ page }, testInfo expect(metadata.sourceRange.start).toBeLessThan(2200); }); -test("manual start time overrides the default", async ({ page }, testInfo) => { +test("manual start time overrides the default", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 700, highlight: false }); const metadata = await recordStartTimeline({ manualStartAtMs: 1400, - page, + basePage, testInfo, video, }); @@ -31,7 +31,7 @@ test("manual start time overrides the default", async ({ page }, testInfo) => { expect(metadata.sourceRange.start).toBeLessThan(1700); }); -test("selector start begins when its marker becomes visible", async ({ page }, testInfo) => { +test("selector start begins when its marker becomes visible", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 700, highlight: false, @@ -40,7 +40,7 @@ test("selector start begins when its marker becomes visible", async ({ page }, t const metadata = await recordStartTimeline({ manualStartAtMs: false, - page, + basePage, testInfo, video, }); @@ -49,12 +49,12 @@ test("selector start begins when its marker becomes visible", async ({ page }, t expect(metadata.sourceRange.start).toBeLessThan(1900); }); -test("blank detection begins when the loading shell paints", async ({ page }, testInfo) => { +test("blank detection begins when the loading shell paints", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 700, highlight: false, trimStart: "detect-blank" }); const metadata = await recordStartTimeline({ manualStartAtMs: false, - page, + basePage, testInfo, video, }); @@ -63,12 +63,12 @@ test("blank detection begins when the loading shell paints", async ({ page }, te expect(metadata.sourceRange.start).toBeLessThan(1800); }); -test('trimStart: "never" keeps the whole recording', async ({ page }, testInfo) => { +test('trimStart: "never" keeps the whole recording', async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 700, highlight: false, trimStart: "never" }); const metadata = await recordStartTimeline({ manualStartAtMs: false, - page, + basePage, testInfo, video, }); @@ -78,29 +78,29 @@ test('trimStart: "never" keeps the whole recording', async ({ page }, testInfo) const recordStartTimeline = async (options: { manualStartAtMs: false | number; - page: any; + basePage: any; testInfo: any; video: ReturnType; }) => { { - await using plugged = await addPlugins({ - page: options.page, + await using page = await addPlugins({ + page: options.basePage, testInfo: options.testInfo, plugins: [options.video], }); - await plugged.setViewportSize({ width: 800, height: 450 }); - await plugged.setContent(startTimelinePage); + await page.setViewportSize({ width: 800, height: 450 }); + await page.setContent(startTimelinePage); if (options.manualStartAtMs !== false) { - await plugged.waitForTimeout(options.manualStartAtMs); - plugged.videoMode.setStartTime(); - await plugged.waitForTimeout(FIRST_LOCATOR_AT_MS - options.manualStartAtMs); + await page.waitForTimeout(options.manualStartAtMs); + page.videoMode.setStartTime(); + await page.waitForTimeout(FIRST_LOCATOR_AT_MS - options.manualStartAtMs); } else { - await plugged.waitForTimeout(FIRST_LOCATOR_AT_MS); + await page.waitForTimeout(FIRST_LOCATOR_AT_MS); } - await plugged.locator("#ready").waitFor(); - await plugged.waitForTimeout(400); + await page.locator("#ready").waitFor(); + await page.waitForTimeout(400); } return await options.video.metadata(); diff --git a/spec/video-mode.spec.ts b/spec/video-mode.spec.ts index a8e2cce..f819b3e 100644 --- a/spec/video-mode.spec.ts +++ b/spec/video-mode.spec.ts @@ -3,10 +3,10 @@ import { test, expect } from "@playwright/test"; import { addPlugins, videoMode } from "../src/index.ts"; test("records goto destinations without changing the live page", async ({ - page, + page: basePage, }, testInfo) => { const destination = "https://app.middlewright.test/reports?period=this-week"; - await page.route(destination, async (route) => { + await basePage.route(destination, async (route) => { await route.fulfill({ body: `
Weekly reports
@@ -27,18 +27,18 @@ test("records goto destinations without changing the live page", async ({ finalHold: 0, highlight: false, }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); const startedAt = performance.now(); - await plugged.goto(destination); + await page.goto(destination); expect(performance.now() - startedAt).toBeLessThan(2000); - await expect(plugged.getByRole("main")).toHaveText("Weekly reports"); - expect(await plugged.evaluate(() => (window as any).addressBarEnteredPage)).toBe(false); + await basePage.waitForSelector('main:has-text("Weekly reports")'); + expect(await page.evaluate(() => (window as any).addressBarEnteredPage)).toBe(false); await expect(video.metadata()).resolves.toMatchObject({ addressBars: [ { @@ -51,19 +51,19 @@ test("records goto destinations without changing the live page", async ({ }); test("keeps a successful fill when its reveal target disappears", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "outline", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.getByLabel("Search").fill("middlewright"); + await page.getByLabel("Search").fill("middlewright"); - await expect(plugged.locator("output")).toHaveText("middlewright"); + await basePage.waitForSelector('output:has-text("middlewright")'); const metadata = await video.metadata(); expect(metadata).toMatchObject({ highlights: [ @@ -89,17 +89,17 @@ test("keeps a successful fill when its reveal target disappears", async ({ expect(metadata.highlights[0]).not.toHaveProperty("fillReveal"); }); -test("records Playwright test steps as captions by default", async ({ page }, testInfo) => { +test("records Playwright test steps as captions by default", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: false }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); await test.step("Create an account", async () => { - await plugged.setContent(""); - await plugged.getByText("Create").click(); + await page.setContent(""); + await page.getByText("Create").click(); }); await expect(video.metadata()).resolves.toMatchObject({ @@ -116,22 +116,22 @@ test("records Playwright test steps as captions by default", async ({ page }, te ); }); -test("records only explicit captions when configured", async ({ page }, testInfo) => { +test("records only explicit captions when configured", async ({ page: basePage }, testInfo) => { const video = videoMode({ captions: "explicit", finalHold: 0, highlight: false, }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); const result = await test.step("Ignored Playwright step", async () => { - return await plugged.videoMode.caption("Create an account", async () => { - await plugged.setContent(""); - await plugged.getByText("Create").click(); + return await page.videoMode.caption("Create an account", async () => { + await page.setContent(""); + await page.getByText("Create").click(); return "created"; }); }); @@ -148,24 +148,24 @@ test("records only explicit captions when configured", async ({ page }, testInfo }); }); -test("shows the innermost caption and resumes its parent", async ({ page }, testInfo) => { +test("shows the innermost caption and resumes its parent", async ({ page: basePage }, testInfo) => { const video = videoMode({ captions: "explicit", finalHold: 0, highlight: false, }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.videoMode.caption("Create an account", async () => { - await plugged.waitForTimeout(10); - await plugged.videoMode.caption("Choose a plan", async () => { - await plugged.waitForTimeout(10); + await page.videoMode.caption("Create an account", async () => { + await page.waitForTimeout(10); + await page.videoMode.caption("Choose a plan", async () => { + await page.waitForTimeout(10); }); - await plugged.waitForTimeout(10); + await page.waitForTimeout(10); }); const captions = (await video.metadata()).captions; @@ -179,14 +179,14 @@ test("shows the innermost caption and resumes its parent", async ({ page }, test }); test("records highlight metadata without mutating element styles", async ({ - page, + page: basePage, }, testInfo) => { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 300 } })], }); - await plugged.setContent(` + await page.setContent(`
`); - plugged.once("dialog", (dialog) => dialog.accept()); + page.once("dialog", (dialog) => dialog.accept()); - await plugged.locator("#discard").click(); + await page.locator("#discard").click(); - await expect(plugged.locator("#result")).toHaveText("discarded"); + await basePage.waitForSelector('#result:has-text("discarded")'); await expect(video.metadata()).resolves.toMatchObject({ highlights: expect.arrayContaining([ expect.objectContaining({ @@ -268,18 +268,18 @@ test("records an accepted confirm as a synthetic dialog annotation", async ({ }); }); -test("records prompt entry before the accepted prompt decision", async ({ page }, testInfo) => { +test("records prompt entry before the accepted prompt decision", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - plugged.once("dialog", (dialog) => dialog.accept("release-notes.md")); + page.once("dialog", (dialog) => dialog.accept("release-notes.md")); - await plugged.locator("#rename").click(); + await page.locator("#rename").click(); - await expect(plugged.locator("#result")).toHaveText("release-notes.md"); + await basePage.waitForSelector('#result:has-text("release-notes.md")'); const metadata = await video.metadata(); const dialogHighlights = metadata.highlights.filter( (candidate) => candidate.dialog?.type === "prompt", @@ -325,19 +325,19 @@ test("records prompt entry before the accepted prompt decision", async ({ page } }); test("records an explicit empty prompt response separately from its default", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - plugged.once("dialog", (dialog) => dialog.accept("")); + page.once("dialog", (dialog) => dialog.accept("")); - await plugged.locator("#rename").click(); + await page.locator("#rename").click(); - await expect(plugged.locator("#result")).toHaveText('""'); + await basePage.waitForSelector('#result:has-text(\'""\')'); const dialogHighlights = (await video.metadata()).highlights.filter( (candidate) => candidate.dialog?.type === "prompt", ); @@ -380,18 +380,18 @@ test("records an explicit empty prompt response separately from its default", as ]); }); -test("preserves Playwright's automatic dialog dismissal", async ({ page }, testInfo) => { +test("preserves Playwright's automatic dialog dismissal", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#discard").click(); + await page.locator("#discard").click(); - await expect(plugged.locator("#result")).toHaveText("kept"); + await basePage.waitForSelector('#result:has-text("kept")'); await expect(video.metadata()).resolves.toMatchObject({ highlights: expect.arrayContaining([ expect.objectContaining({ @@ -418,18 +418,18 @@ test("preserves Playwright's automatic dialog dismissal", async ({ page }, testI }); }); -test("records an alert acknowledgement", async ({ page }, testInfo) => { +test("records an alert acknowledgement", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - plugged.once("dialog", (dialog) => dialog.accept()); + page.once("dialog", (dialog) => dialog.accept()); - await plugged.locator("#publish").click(); + await page.locator("#publish").click(); - await expect(plugged.locator("#result")).toHaveText("done"); + await basePage.waitForSelector('#result:has-text("done")'); await expect(video.metadata()).resolves.toMatchObject({ highlights: expect.arrayContaining([ expect.objectContaining({ @@ -458,18 +458,18 @@ test("records an alert acknowledgement", async ({ page }, testInfo) => { }); }); -test("records automatic alert dismissal as an OK acknowledgement", async ({ page }, testInfo) => { +test("records automatic alert dismissal as an OK acknowledgement", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#publish").click(); + await page.locator("#publish").click(); - await expect(plugged.locator("#result")).toHaveText("acknowledged"); + await basePage.waitForSelector('#result:has-text("acknowledged")'); await expect(video.metadata()).resolves.toMatchObject({ highlights: expect.arrayContaining([ expect.objectContaining({ @@ -497,20 +497,20 @@ test("records automatic alert dismissal as an OK acknowledgement", async ({ page }); test("records dialogs handled by a listener registered before video mode", async ({ - page, + page: basePage, }, testInfo) => { - page.once("dialog", (dialog) => dialog.accept()); + basePage.once("dialog", (dialog) => dialog.accept()); const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#continue").click(); + await page.locator("#continue").click(); - await expect(plugged.locator("#result")).toHaveText("yes"); + await basePage.waitForSelector('#result:has-text("yes")'); expect((await video.metadata()).highlights).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -532,18 +532,18 @@ test("records dialogs handled by a listener registered before video mode", async ); }); -test("records back-to-back dialogs in order", async ({ page }, testInfo) => { +test("records back-to-back dialogs in order", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 0, highlight: { mode: "pointer", duration: 300 }, trimStart: "never", }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - plugged.on("dialog", (dialog) => void dialog.accept()); + page.on("dialog", (dialog) => void dialog.accept()); - await plugged.locator("#discard").click(); + await page.locator("#discard").click(); - await expect(plugged.locator("#result")).toHaveText("discarded"); + await basePage.waitForSelector('#result:has-text("discarded")'); const dialogHighlights = (await video.metadata()).highlights.filter( (highlight) => highlight.dialog, ); @@ -571,18 +571,18 @@ test("records back-to-back dialogs in order", async ({ page }, testInfo) => { expect(dialogHighlights.map((highlight) => highlight.image)).toEqual([undefined, undefined]); }); -test("skipped methods are not highlighted", async ({ page }, testInfo) => { +test("skipped methods are not highlighted", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 5000 }, skipMethods: ["click"], }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#late").click(); + await page.locator("#late").click(); - await expect(plugged.locator("#result")).toContainText("clicked"); + await basePage.waitForSelector('#result:has-text("clicked")'); const metadata = await video.metadata(); expect(metadata.deadAir).toContainEqual( expect.objectContaining({ @@ -662,13 +662,13 @@ test("marks pre-action waits for attachment as dead air", async ({ page }, testI expect(metadata.deadAir.some((span) => span.end - span.start >= 100)).toBe(true); }); -test("pre-action attached waits honor action timeout", async ({ page }, testInfo) => { - await using plugged = await addPlugins({ - page, +test("pre-action attached waits honor action timeout", async ({ page: basePage }, testInfo) => { + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 20 } })], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#late").waitFor({ state: "attached" }); + await page.locator("#late").waitFor({ state: "attached" }); expect((await video.metadata()).deadAir.some((span) => span.end - span.start >= 100)).toBe(true); }); -test("marks default visible waitFor calls as dead air", async ({ page }, testInfo) => { +test("marks default visible waitFor calls as dead air", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 50 }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#ready").waitFor(); + await page.locator("#ready").waitFor(); const metadata = await video.metadata(); expect(metadata.deadAir.some((span) => span.end - span.start >= 100)).toBe(true); @@ -728,14 +728,14 @@ test("marks default visible waitFor calls as dead air", async ({ page }, testInf expect(metadata.highlights[0].end - metadata.highlights[0].start).toBe(1000); }); -test("marks explicit visible waitFor calls as dead air", async ({ page }, testInfo) => { +test("marks explicit visible waitFor calls as dead air", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 20 } }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(` `); - await plugged.locator("#late").waitFor({ state: "visible" }); + await page.locator("#late").waitFor({ state: "visible" }); expect((await video.metadata()).deadAir.some((span) => span.end - span.start >= 100)).toBe(true); }); -test("does not highlight a waitFor result that is no longer visible", async ({ page }, testInfo) => { +test("does not highlight a waitFor result that is no longer visible", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 20 } }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(`
Temporary notice
`); - await plugged.locator("#notice").waitFor({ state: "hidden" }); + await page.locator("#notice").waitFor({ state: "hidden" }); const metadata = await video.metadata(); expect(metadata.deadAir.some((span) => span.end - span.start >= 100)).toBe(true); expect(metadata.highlights).toEqual([]); }); -test("marks attached actionability waits as dead air", async ({ page }, testInfo) => { +test("marks attached actionability waits as dead air", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 20 } }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(`
`); - await plugged.locator("#ready").click(); + await page.locator("#ready").click(); - await expect(plugged.locator("#result")).toContainText("clicked"); + await basePage.waitForSelector('#result:has-text("clicked")'); expect((await video.metadata()).deadAir.some((span) => span.end - span.start >= 100)).toBe(true); }); -test("sets video source range from current timestamps", async ({ page }, testInfo) => { +test("sets video source range from current timestamps", async ({ page: basePage }, testInfo) => { const video = videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 20 } }); - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - const startBefore = plugged.videoMode.getVideoTimestamp(); - plugged.videoMode.setStartTime(); - const startAfter = plugged.videoMode.getVideoTimestamp(); - await plugged.setContent(``); - await plugged.locator("button").click(); - await plugged.waitForTimeout(20); - const endBefore = plugged.videoMode.getVideoTimestamp(); - plugged.videoMode.setEndTime(); - const endAfter = plugged.videoMode.getVideoTimestamp(); + const startBefore = page.videoMode.getVideoTimestamp(); + page.videoMode.setStartTime(); + const startAfter = page.videoMode.getVideoTimestamp(); + await page.setContent(``); + await page.locator("button").click(); + await page.waitForTimeout(20); + const endBefore = page.videoMode.getVideoTimestamp(); + page.videoMode.setEndTime(); + const endAfter = page.videoMode.getVideoTimestamp(); - const metadata = await plugged.videoMode.metadata(); + const metadata = await page.videoMode.metadata(); expect(metadata).toMatchObject({ sourceRange: { end: expect.any(Number), @@ -829,17 +829,17 @@ test("sets video source range from current timestamps", async ({ page }, testInf }); test("deadAir runs actions without video highlighting and records metadata", async ({ - page, + page: basePage, }, testInfo) => { const video = videoMode({ finalHold: 50, highlight: { mode: "pointer", duration: 5000 } }); { - await using plugged = await addPlugins({ - page, + await using page = await addPlugins({ + page: basePage, testInfo, plugins: [video], }); - await plugged.setContent(` + await page.setContent(`