diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index e068514711..9dfb391f60 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1339,6 +1339,12 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.uncheck.trial = %%-input-trial-%% * since: v1.14 +## async method: Locator.viewportRatio +* since: v1.30 +- returns: <[float]> + +Returns the ratio of intersection between viewport and the element, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + ## async method: Locator.waitFor * since: v1.16 diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 1e31261981..daec154fef 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -257,6 +257,10 @@ export class Locator implements api.Locator { return this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), options.timeout); } + async viewportRatio(): Promise { + return (await this._frame._channel.viewportRatio({ selector: this._selector, strict: true })).value; + } + async selectOption(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise { return this._frame.selectOption(this._selector, values, { strict: true, ...options }); } diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/protocol/debug.ts index cde35816c8..acf4cc7a0c 100644 --- a/packages/playwright-core/src/protocol/debug.ts +++ b/packages/playwright-core/src/protocol/debug.ts @@ -66,6 +66,7 @@ export const commandsWithTracingSnapshots = new Set([ 'Frame.isEnabled', 'Frame.isHidden', 'Frame.isVisible', + 'Frame.viewportRatio', 'Frame.isEditable', 'Frame.press', 'Frame.selectOption', diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 85f6b5bcc8..f11d785f6f 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1425,6 +1425,13 @@ scheme.FrameIsVisibleParams = tObject({ scheme.FrameIsVisibleResult = tObject({ value: tBoolean, }); +scheme.FrameViewportRatioParams = tObject({ + selector: tString, + strict: tOptional(tBoolean), +}); +scheme.FrameViewportRatioResult = tObject({ + value: tNumber, +}); scheme.FrameIsEditableParams = 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 8098f3c934..c8fbf67bd4 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -203,6 +203,10 @@ export class FrameDispatcher extends Dispatcher { + return { value: await this._frame.viewportRatio(metadata, params.selector, params) }; + } + async hover(params: channels.FrameHoverParams, metadata: CallMetadata): Promise { return await this._frame.hover(metadata, params.selector, params); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b389a92ee2..9137b5f5f0 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1318,6 +1318,31 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout({})); } + async viewportRatio(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}): Promise { + const controller = new ProgressController(metadata, this); + return controller.run(async progress => { + progress.log(` calculating viewport ratio of ${this._asLocator(selector)}`); + const resolved = await this._resolveInjectedForSelector(progress, selector, options); + if (!resolved) + return 0; + return await resolved.injected.evaluate(async (injected, { info }) => { + const element = injected.querySelector(info.parsed, document, info.strict); + if (!element) + return 0; + return await new Promise(resolve => { + const observer = new IntersectionObserver(entries => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + // Firefox doesn't call IntersectionObserver callback unless + // there are rafs. + requestAnimationFrame(() => {}); + }); + }, { info: resolved.info }); + }, this._page._timeoutSettings.timeout({})); + } + async isHidden(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}): Promise { return !(await this.isVisible(metadata, selector, options)); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 8e50da0ce0..166cf47d76 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -11448,6 +11448,12 @@ export interface Locator { trial?: boolean; }): Promise; + /** + * Returns the ratio of intersection between viewport and the element, according to the + * [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + */ + viewportRatio(): Promise; + /** * Returns when element specified by locator satisfies the `state` option. * diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 70168edfb1..42e96c7eef 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2196,6 +2196,7 @@ export interface FrameChannel extends FrameEventTarget, Channel { isEnabled(params: FrameIsEnabledParams, metadata?: Metadata): Promise; isHidden(params: FrameIsHiddenParams, metadata?: Metadata): Promise; isVisible(params: FrameIsVisibleParams, metadata?: Metadata): Promise; + viewportRatio(params: FrameViewportRatioParams, metadata?: Metadata): Promise; isEditable(params: FrameIsEditableParams, metadata?: Metadata): Promise; press(params: FramePressParams, metadata?: Metadata): Promise; querySelector(params: FrameQuerySelectorParams, metadata?: Metadata): Promise; @@ -2593,6 +2594,16 @@ export type FrameIsVisibleOptions = { export type FrameIsVisibleResult = { value: boolean, }; +export type FrameViewportRatioParams = { + selector: string, + strict?: boolean, +}; +export type FrameViewportRatioOptions = { + strict?: boolean, +}; +export type FrameViewportRatioResult = { + value: number, +}; export type FrameIsEditableParams = { selector: string, strict?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index acfcf8223f..bf553adc2c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1908,6 +1908,15 @@ Frame: tracing: snapshot: true + viewportRatio: + parameters: + selector: string + strict: boolean? + returns: + value: number + tracing: + snapshot: true + isEditable: parameters: selector: string diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index c15c809dd6..b9fcaea4c2 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -154,3 +154,17 @@ it('locator.count should work with deleted Map in main world', async ({ page }) await page.locator('#searchResultTableDiv .x-grid3-row').count(); await expect(page.locator('#searchResultTableDiv .x-grid3-row')).toHaveCount(0); }); + +it('locator.viewportRatio', async ({ page }) => { + await page.setContent(` + +
+
+
+
+ `); + expect.soft(await page.locator('#fills-viewport').viewportRatio()).toBe(1); + expect.soft(await page.locator('#half-viewport').viewportRatio()).toBe(1); + expect.soft(await page.locator('#twice-viewport').viewportRatio()).toBe(0.5); + expect.soft(await page.locator('#off-viewport').viewportRatio()).toBe(0); +});