diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index d03154ee68..af035d6f1a 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -18,6 +18,14 @@ Returns an array of `node.innerText` values for all matching nodes. Returns an array of `node.textContent` values for all matching nodes. +## async method: Locator.blur +* since: v1.28 + +Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element. + +### option: Locator.blur.timeout = %%-input-timeout-%% +* since: v1.28 + ## async method: Locator.boundingBox * since: v1.14 - returns: <[null]|[Object]> diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index b0a0339a44..4058eeea53 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -198,6 +198,10 @@ export class Locator implements api.Locator { return this._frame.focus(this._selector, { strict: true, ...options }); } + async blur(options?: TimeoutOptions): Promise { + await this._frame._channel.blur({ selector: this._selector, strict: true, ...options }); + } + async count(): Promise { return this._frame._queryCount(this._selector); } diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/protocol/debug.ts index 5be3acabc4..b3f461b8f2 100644 --- a/packages/playwright-core/src/protocol/debug.ts +++ b/packages/playwright-core/src/protocol/debug.ts @@ -44,6 +44,7 @@ export const commandsWithTracingSnapshots = new Set([ 'Frame.evalOnSelectorAll', 'Frame.addScriptTag', 'Frame.addStyleTag', + 'Frame.blur', 'Frame.check', 'Frame.click', 'Frame.dragAndDrop', diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 626f918531..44cca2fcb7 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1203,6 +1203,12 @@ scheme.FrameAddStyleTagParams = tObject({ scheme.FrameAddStyleTagResult = tObject({ element: tChannel(['ElementHandle']), }); +scheme.FrameBlurParams = tObject({ + selector: tString, + strict: tOptional(tBoolean), + timeout: tOptional(tNumber), +}); +scheme.FrameBlurResult = tOptional(tObject({})); scheme.FrameCheckParams = tObject({ selector: tString, strict: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 903e8f9d0f..413249e08d 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -152,6 +152,10 @@ export class FrameDispatcher extends Dispatcher { + await this._frame.blur(metadata, params.selector, params); + } + async textContent(params: channels.FrameTextContentParams, metadata: CallMetadata): Promise { const value = await this._frame.textContent(metadata, params.selector, params); return { value: value === null ? undefined : value }; diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index db33b20114..9d8b9fd7a1 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -668,6 +668,11 @@ export class ElementHandle extends js.JSHandle { return await this.evaluateInUtility(([injected, node, resetSelectionIfNotFocused]) => injected.focusNode(node, resetSelectionIfNotFocused), resetSelectionIfNotFocused); } + async _blur(progress: Progress): Promise<'error:notconnected' | 'done'> { + progress.throwIfAborted(); // Avoid action that has side-effects. + return await this.evaluateInUtility(([injected, node]) => injected.blurNode(node), {}); + } + async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 20520c0168..3cc44c0446 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1191,6 +1191,14 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } + async blur(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}) { + const controller = new ProgressController(metadata, this); + await controller.run(async progress => { + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options.strict, handle => handle._blur(progress))); + await this._page._doSlowMo(); + }, this._page._timeoutSettings.timeout(options)); + } + async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise { return this._scheduleRerunnableTask(metadata, selector, (progress, element) => element.textContent, undefined, options); } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 183b3cabfb..1377e73647 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -766,6 +766,15 @@ export class InjectedScript { return 'done'; } + blurNode(node: Node): 'error:notconnected' | 'done' { + if (!node.isConnected) + return 'error:notconnected'; + if (node.nodeType !== Node.ELEMENT_NODE) + throw this.createStacklessError('Node is not an element'); + (node as HTMLElement | SVGElement).blur(); + return 'done'; + } + setInputFiles(node: Node, payloads: { name: string, mimeType: string, buffer: string }[]) { if (node.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement'; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index dd0fdcbd84..ac356e0e0c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9487,6 +9487,20 @@ export interface Locator { */ allTextContents(): Promise>; + /** + * Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element. + * @param options + */ + blur(options?: { + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by + * using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number; + }): Promise; + /** * This method returns the bounding box of the element, or `null` if the element is not visible. The bounding box is * calculated relative to the main frame viewport - which is usually the same as the browser window. diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 650c158326..d5c1279a99 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2131,6 +2131,7 @@ export interface FrameChannel extends FrameEventTarget, Channel { evalOnSelectorAll(params: FrameEvalOnSelectorAllParams, metadata?: Metadata): Promise; addScriptTag(params: FrameAddScriptTagParams, metadata?: Metadata): Promise; addStyleTag(params: FrameAddStyleTagParams, metadata?: Metadata): Promise; + blur(params: FrameBlurParams, metadata?: Metadata): Promise; check(params: FrameCheckParams, metadata?: Metadata): Promise; click(params: FrameClickParams, metadata?: Metadata): Promise; content(params?: FrameContentParams, metadata?: Metadata): Promise; @@ -2235,6 +2236,16 @@ export type FrameAddStyleTagOptions = { export type FrameAddStyleTagResult = { element: ElementHandleChannel, }; +export type FrameBlurParams = { + selector: string, + strict?: boolean, + timeout?: number, +}; +export type FrameBlurOptions = { + strict?: boolean, + timeout?: number, +}; +export type FrameBlurResult = void; export type FrameCheckParams = { selector: string, strict?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 3286f217ab..02922cfcea 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1586,6 +1586,14 @@ Frame: tracing: snapshot: true + blur: + parameters: + selector: string + strict: boolean? + timeout: number? + tracing: + snapshot: true + check: parameters: selector: string diff --git a/tests/page/locator-misc-1.spec.ts b/tests/page/locator-misc-1.spec.ts index bd04cf86a7..2f737f7ce1 100644 --- a/tests/page/locator-misc-1.spec.ts +++ b/tests/page/locator-misc-1.spec.ts @@ -89,12 +89,29 @@ it('should select single option', async ({ page, server }) => { expect(await page.evaluate(() => window['result'].onChange)).toEqual(['blue']); }); -it('should focus a button', async ({ page, server }) => { +it('should focus and blur a button', async ({ page, server }) => { await page.goto(server.PREFIX + '/input/button.html'); const button = page.locator('button'); expect(await button.evaluate(button => document.activeElement === button)).toBe(false); + + let focused = false; + let blurred = false; + await page.exposeFunction('focusEvent', () => focused = true); + await page.exposeFunction('blurEvent', () => blurred = true); + await button.evaluate(button => { + button.addEventListener('focus', window['focusEvent']); + button.addEventListener('blur', window['blurEvent']); + }); + await button.focus(); + expect(focused).toBe(true); + expect(blurred).toBe(false); expect(await button.evaluate(button => document.activeElement === button)).toBe(true); + + await button.blur(); + expect(focused).toBe(true); + expect(blurred).toBe(true); + expect(await button.evaluate(button => document.activeElement === button)).toBe(false); }); it('focus should respect strictness', async ({ page, server }) => {