diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 39610ae84d..85e5af5929 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3144,10 +3144,6 @@ return value resolves to `[]`. ## async method: Page.addLocatorHandler * since: v1.42 -:::warning[Experimental] -This method is experimental and its behavior may change in the upcoming releases. -::: - When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making them tricky to handle in automated tests. This method lets you set up a special function, called a handler, that activates when it detects that overlay is visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there. @@ -3155,7 +3151,7 @@ This method lets you set up a special function, called a handler, that activates Things to keep in mind: * When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as a part of your normal test flow, instead of using [`method: Page.addLocatorHandler`]. * Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](../actionability.md), or before performing an auto-waiting assertion check. When overlay is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't perform any actions, the handler will not be triggered. -* After executing the handler, Playwright will ensure that overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with [`option: allowStayingVisible`]. +* After executing the handler, Playwright will ensure that overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with [`option: noWaitAfter`]. * The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts. * You can register multiple handlers. However, only a single handler will be running at a time. Make sure the actions within a handler don't depend on another handler. @@ -3285,13 +3281,13 @@ await page.GotoAsync("https://example.com"); await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync(); ``` -An example with a custom callback on every actionability check. It uses a `` locator that is always visible, so the handler is called before every actionability check. It is important to specify [`option: allowStayingVisible`], because the handler does not hide the `` element. +An example with a custom callback on every actionability check. It uses a `` locator that is always visible, so the handler is called before every actionability check. It is important to specify [`option: noWaitAfter`], because the handler does not hide the `` element. ```js // Setup the handler. await page.addLocatorHandler(page.locator('body'), async () => { await page.evaluate(() => window.removeObstructionsForTestIfNeeded()); -}, { allowStayingVisible: true }); +}, { noWaitAfter: true }); // Write the test as usual. await page.goto('https://example.com'); @@ -3302,7 +3298,7 @@ await page.getByRole('button', { name: 'Start here' }).click(); // Setup the handler. page.addLocatorHandler(page.locator("body")), () => { page.evaluate("window.removeObstructionsForTestIfNeeded()"); -}, new Page.AddLocatorHandlerOptions.setAllowStayingVisible(true)); +}, new Page.AddLocatorHandlerOptions.setNoWaitAfter(true)); // Write the test as usual. page.goto("https://example.com"); @@ -3313,7 +3309,7 @@ page.getByRole("button", Page.GetByRoleOptions().setName("Start here")).click(); # Setup the handler. def handler(): page.evaluate("window.removeObstructionsForTestIfNeeded()") -page.add_locator_handler(page.locator("body"), handler, allow_staying_visible=True) +page.add_locator_handler(page.locator("body"), handler, no_wait_after=True) # Write the test as usual. page.goto("https://example.com") @@ -3324,7 +3320,7 @@ page.get_by_role("button", name="Start here").click() # Setup the handler. def handler(): await page.evaluate("window.removeObstructionsForTestIfNeeded()") -await page.add_locator_handler(page.locator("body"), handler, allow_staying_visible=True) +await page.add_locator_handler(page.locator("body"), handler, no_wait_after=True) # Write the test as usual. await page.goto("https://example.com") @@ -3335,7 +3331,7 @@ await page.get_by_role("button", name="Start here").click() // Setup the handler. await page.AddLocatorHandlerAsync(page.Locator("body"), async () => { await page.EvaluateAsync("window.removeObstructionsForTestIfNeeded()"); -}, new() { AllowStayingVisible = true }); +}, new() { NoWaitAfter = true }); // Write the test as usual. await page.GotoAsync("https://example.com"); @@ -3407,9 +3403,9 @@ Function that should be run once [`param: locator`] appears. This function shoul Specifies the maximum number of times this handler should be called. Unlimited by default. -### option: Page.addLocatorHandler.allowStayingVisible +### option: Page.addLocatorHandler.noWaitAfter * since: v1.44 -- `allowStayingVisible` <[boolean]> +- `noWaitAfter` <[boolean]> By default, after calling the handler Playwright will wait until the overlay becomes hidden, and only then Playwright will continue with the action/assertion that triggered the handler. This option allows to opt-out of this behavior, so that overlay can stay visible after the handler has run. @@ -3417,11 +3413,7 @@ By default, after calling the handler Playwright will wait until the overlay bec ## async method: Page.removeLocatorHandler * since: v1.44 -:::warning[Experimental] -This method is experimental and its behavior may change in the upcoming releases. -::: - -Removes locator handler added by [`method: Page.addLocatorHandler`]. +Removes all locator handlers added by [`method: Page.addLocatorHandler`] for a specific locator. ### param: Page.removeLocatorHandler.locator * since: v1.44 @@ -3429,20 +3421,6 @@ Removes locator handler added by [`method: Page.addLocatorHandler`]. Locator passed to [`method: Page.addLocatorHandler`]. -### param: Page.removeLocatorHandler.handler -* langs: js, python -* since: v1.44 -- `handler` <[function]\([Locator]\): [Promise]> - -Handler passed to [`method: Page.addLocatorHandler`]. - -### param: Page.addLocatorHandler.handler -* langs: csharp, java -* since: v1.44 -- `handler` <[function]\([Locator]\)> - -Handler passed to [`method: Page.addLocatorHandler`]. - ## async method: Page.reload * since: v1.8 diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 254d4fbf6e..de5aaff6b1 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -362,12 +362,12 @@ export class Page extends ChannelOwner implements api.Page return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response); } - async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, allowStayingVisible?: boolean } = {}): Promise { + async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, noWaitAfter?: boolean } = {}): Promise { if (locator._frame !== this._mainFrame) throw new Error(`Locator must belong to the main frame of this page`); if (options.times === 0) return; - const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, allowStayingVisible: options.allowStayingVisible }); + const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, noWaitAfter: options.noWaitAfter }); this._locatorHandlers.set(uid, { locator, handler, times: options.times }); } @@ -386,11 +386,11 @@ export class Page extends ChannelOwner implements api.Page } } - async removeLocatorHandler(locator: Locator, handler: (locator: Locator) => any): Promise { + async removeLocatorHandler(locator: Locator): Promise { for (const [uid, data] of this._locatorHandlers) { - if (data.locator._equals(locator) && data.handler === handler) { + if (data.locator._equals(locator)) { this._locatorHandlers.delete(uid); - await this._channel.unregisterLocatorHandlerNoReply({ uid }).catch(() => {}); + await this._channel.unregisterLocatorHandler({ uid }).catch(() => {}); } } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index d060718c50..20f56e8d89 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1046,7 +1046,7 @@ scheme.PageGoForwardResult = tObject({ }); scheme.PageRegisterLocatorHandlerParams = tObject({ selector: tString, - allowStayingVisible: tOptional(tBoolean), + noWaitAfter: tOptional(tBoolean), }); scheme.PageRegisterLocatorHandlerResult = tObject({ uid: tNumber, @@ -1056,10 +1056,10 @@ scheme.PageResolveLocatorHandlerNoReplyParams = tObject({ remove: tOptional(tBoolean), }); scheme.PageResolveLocatorHandlerNoReplyResult = tOptional(tObject({})); -scheme.PageUnregisterLocatorHandlerNoReplyParams = tObject({ +scheme.PageUnregisterLocatorHandlerParams = tObject({ uid: tNumber, }); -scheme.PageUnregisterLocatorHandlerNoReplyResult = tOptional(tObject({})); +scheme.PageUnregisterLocatorHandlerResult = tOptional(tObject({})); scheme.PageReloadParams = tObject({ timeout: tOptional(tNumber), waitUntil: tOptional(tType('LifecycleEvent')), diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index d873d8d06a..3101cd051d 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -138,7 +138,7 @@ export class PageDispatcher extends Dispatcher { - const uid = this._page.registerLocatorHandler(params.selector, params.allowStayingVisible); + const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter); return { uid }; } @@ -146,7 +146,7 @@ export class PageDispatcher extends Dispatcher { + async unregisterLocatorHandler(params: channels.PageUnregisterLocatorHandlerParams, metadata: CallMetadata): Promise { this._page.unregisterLocatorHandler(params.uid); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 4bc8730b26..07a3df8fc5 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -168,7 +168,7 @@ export class Page extends SdkObject { _video: Artifact | null = null; _opener: Page | undefined; private _isServerSideOnly = false; - private _locatorHandlers = new Map }>(); + private _locatorHandlers = new Map }>(); private _lastLocatorHandlerUid = 0; private _locatorHandlerRunningCounter = 0; @@ -432,9 +432,9 @@ export class Page extends SdkObject { }), this._timeoutSettings.navigationTimeout(options)); } - registerLocatorHandler(selector: string, allowStayingVisible: boolean | undefined) { + registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) { const uid = ++this._lastLocatorHandlerUid; - this._locatorHandlers.set(uid, { selector, allowStayingVisible }); + this._locatorHandlers.set(uid, { selector, noWaitAfter }); return uid; } @@ -468,8 +468,12 @@ export class Page extends SdkObject { progress.log(` found ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)}, intercepting action to run the handler`); const promise = handler.resolved.then(async () => { progress.throwIfAborted(); - if (!handler.allowStayingVisible) + if (!handler.noWaitAfter) { + progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`); await this.mainFrame().waitForSelectorInternal(progress, handler.selector, { state: 'hidden' }); + } else { + progress.log(` locator handler has finished`); + } }); await this.openScope.race(promise).finally(() => --this._locatorHandlerRunningCounter); // Avoid side-effects after long-running operation. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index fbc4105513..0222aea14c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -1790,8 +1790,6 @@ export interface Page { prependListener(event: 'worker', listener: (worker: Worker) => any): this; /** - * **NOTE** This method is experimental and its behavior may change in the upcoming releases. - * * When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to * automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making * them tricky to handle in automated tests. @@ -1809,7 +1807,7 @@ export interface Page { * handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't * perform any actions, the handler will not be triggered. * - After executing the handler, Playwright will ensure that overlay that triggered the handler is not visible - * anymore. You can opt-out of this behavior with `allowStayingVisible`. + * anymore. You can opt-out of this behavior with `noWaitAfter`. * - The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. * If your handler takes too long, it might cause timeouts. * - You can register multiple handlers. However, only a single handler will be running at a time. Make sure the @@ -1859,14 +1857,14 @@ export interface Page { * ``` * * An example with a custom callback on every actionability check. It uses a `` locator that is always visible, - * so the handler is called before every actionability check. It is important to specify `allowStayingVisible`, - * because the handler does not hide the `` element. + * so the handler is called before every actionability check. It is important to specify `noWaitAfter`, because the + * handler does not hide the `` element. * * ```js * // Setup the handler. * await page.addLocatorHandler(page.locator('body'), async () => { * await page.evaluate(() => window.removeObstructionsForTestIfNeeded()); - * }, { allowStayingVisible: true }); + * }, { noWaitAfter: true }); * * // Write the test as usual. * await page.goto('https://example.com'); @@ -1893,7 +1891,7 @@ export interface Page { * Playwright will continue with the action/assertion that triggered the handler. This option allows to opt-out of * this behavior, so that overlay can stay visible after the handler has run. */ - allowStayingVisible?: boolean; + noWaitAfter?: boolean; /** * Specifies the maximum number of times this handler should be called. Unlimited by default. @@ -3680,16 +3678,13 @@ export interface Page { }): Promise; /** - * **NOTE** This method is experimental and its behavior may change in the upcoming releases. - * - * Removes locator handler added by - * [page.addLocatorHandler(locator, handler[, options])](https://playwright.dev/docs/api/class-page#page-add-locator-handler). + * Removes all locator handlers added by + * [page.addLocatorHandler(locator, handler[, options])](https://playwright.dev/docs/api/class-page#page-add-locator-handler) + * for a specific locator. * @param locator Locator passed to * [page.addLocatorHandler(locator, handler[, options])](https://playwright.dev/docs/api/class-page#page-add-locator-handler). - * @param handler Handler passed to - * [page.addLocatorHandler(locator, handler[, options])](https://playwright.dev/docs/api/class-page#page-add-locator-handler). */ - removeLocatorHandler(locator: Locator, handler: ((locator: Locator) => Promise)): Promise; + removeLocatorHandler(locator: Locator): Promise; /** * Routing provides the capability to modify network requests that are made by a page. diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 6a4ab7be44..d9c1ec6ad9 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1790,7 +1790,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise; registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise; resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise; - unregisterLocatorHandlerNoReply(params: PageUnregisterLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise; + unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise; reload(params: PageReloadParams, metadata?: CallMetadata): Promise; expectScreenshot(params: PageExpectScreenshotParams, metadata?: CallMetadata): Promise; screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise; @@ -1927,10 +1927,10 @@ export type PageGoForwardResult = { }; export type PageRegisterLocatorHandlerParams = { selector: string, - allowStayingVisible?: boolean, + noWaitAfter?: boolean, }; export type PageRegisterLocatorHandlerOptions = { - allowStayingVisible?: boolean, + noWaitAfter?: boolean, }; export type PageRegisterLocatorHandlerResult = { uid: number, @@ -1943,13 +1943,13 @@ export type PageResolveLocatorHandlerNoReplyOptions = { remove?: boolean, }; export type PageResolveLocatorHandlerNoReplyResult = void; -export type PageUnregisterLocatorHandlerNoReplyParams = { +export type PageUnregisterLocatorHandlerParams = { uid: number, }; -export type PageUnregisterLocatorHandlerNoReplyOptions = { +export type PageUnregisterLocatorHandlerOptions = { }; -export type PageUnregisterLocatorHandlerNoReplyResult = void; +export type PageUnregisterLocatorHandlerResult = void; export type PageReloadParams = { timeout?: number, waitUntil?: LifecycleEvent, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 2022489ca6..b2442f9097 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1350,7 +1350,7 @@ Page: registerLocatorHandler: parameters: selector: string - allowStayingVisible: boolean? + noWaitAfter: boolean? returns: uid: number @@ -1359,7 +1359,7 @@ Page: uid: number remove: boolean? - unregisterLocatorHandlerNoReply: + unregisterLocatorHandler: parameters: uid: number diff --git a/tests/page/page-add-locator-handler.spec.ts b/tests/page/page-add-locator-handler.spec.ts index f7ef2c478f..0062ba15ab 100644 --- a/tests/page/page-add-locator-handler.spec.ts +++ b/tests/page/page-add-locator-handler.spec.ts @@ -66,7 +66,7 @@ test('should work with a custom check', async ({ page, server }) => { await page.addLocatorHandler(page.locator('body'), async () => { if (await page.getByText('This interstitial covers the button').isVisible()) await page.locator('#close').click(); - }, { allowStayingVisible: true }); + }, { noWaitAfter: true }); for (const args of [ ['mouseover', 2], @@ -243,7 +243,7 @@ test('should work with times: option', async ({ page, server }) => { let called = 0; await page.addLocatorHandler(page.locator('body'), async () => { ++called; - }, { allowStayingVisible: true, times: 2 }); + }, { noWaitAfter: true, times: 2 }); await page.locator('#aside').hover(); await page.evaluate(() => { @@ -278,7 +278,27 @@ test('should wait for hidden by default', async ({ page, server }) => { expect(called).toBe(1); }); -test('should work with allowStayingVisible', async ({ page, server }) => { +test('should wait for hidden by default 2', async ({ page, server }) => { + await page.goto(server.PREFIX + '/input/handle-locator.html'); + + let called = 0; + await page.addLocatorHandler(page.getByRole('button', { name: 'close' }), async button => { + called++; + }); + + await page.locator('#aside').hover(); + await page.evaluate(() => { + (window as any).clicked = 0; + (window as any).setupAnnoyingInterstitial('hide', 1); + }); + const error = await page.locator('#target').click({ timeout: 3000 }).catch(e => e); + expect(await page.evaluate('window.clicked')).toBe(0); + await expect(page.locator('#interstitial')).toBeVisible(); + expect(called).toBe(1); + expect(error.message).toContain(`locator handler has finished, waiting for getByRole('button', { name: 'close' }) to be hidden`); +}); + +test('should work with noWaitAfter', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/handle-locator.html'); let called = 0; @@ -288,7 +308,7 @@ test('should work with allowStayingVisible', async ({ page, server }) => { await button.click(); else await page.locator('#interstitial').waitFor({ state: 'hidden' }); - }, { allowStayingVisible: true }); + }, { noWaitAfter: true }); await page.locator('#aside').hover(); await page.evaluate(() => { @@ -305,11 +325,10 @@ test('should removeLocatorHandler', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/handle-locator.html'); let called = 0; - const handler = async locator => { + await page.addLocatorHandler(page.getByRole('button', { name: 'close' }), async locator => { ++called; await locator.click(); - }; - await page.addLocatorHandler(page.getByRole('button', { name: 'close' }), handler); + }); await page.evaluate(() => { (window as any).clicked = 0; @@ -324,7 +343,7 @@ test('should removeLocatorHandler', async ({ page, server }) => { (window as any).clicked = 0; (window as any).setupAnnoyingInterstitial('hide', 1); }); - await page.removeLocatorHandler(page.getByRole('button', { name: 'close' }), handler); + await page.removeLocatorHandler(page.getByRole('button', { name: 'close' })); const error = await page.locator('#target').click({ timeout: 3000 }).catch(e => e); expect(called).toBe(1);