Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -2845,6 +2845,24 @@ the place it was paused.
This method requires Playwright to be started in a headed mode, with a falsy [`option: BrowserType.launch.headless`] option.
:::

## async method: Page.pickLocator
* since: v1.59
- returns: <[Locator]>

Enters pick locator mode where hovering over page elements highlights them and shows the corresponding locator.
Once the user clicks an element, the mode is deactivated and the [Locator] for the picked element is returned.

:::note
This method requires Playwright to be started in a headed mode.
:::

**Usage**

```js
const locator = await page.pickLocator();
console.log(locator);
```

## async method: Page.pdf
* since: v1.8
- returns: <[Buffer]>
Expand Down
17 changes: 17 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3904,6 +3904,23 @@ export interface Page {
width?: string|number;
}): Promise<Buffer>;

/**
* Enters pick locator mode where hovering over page elements highlights them and shows the corresponding locator.
* Once the user clicks an element, the mode is deactivated and the
* [Locator](https://playwright.dev/docs/api/class-locator) for the picked element is returned.
*
* **NOTE** This method requires Playwright to be started in a headed mode.
*
* **Usage**
*
* ```js
* const locator = await page.pickLocator();
* console.log(locator);
* ```
*
*/
pickLocator(): Promise<Locator>;

/**
* **NOTE** Use locator-based [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)
* instead. Read more about [locators](https://playwright.dev/docs/locators).
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._browserContext.setDefaultTimeout(defaultTimeout);
}

async pickLocator(): Promise<Locator> {
const { selector } = await this._channel.pickLocator({});
return this.locator(selector);
}

async pdf(options: PDFOptions = {}): Promise<Buffer> {
const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams;
if (transportOptions.margin)
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,10 @@ scheme.PageStopCSSCoverageResult = tObject({
});
scheme.PageBringToFrontParams = tOptional(tObject({}));
scheme.PageBringToFrontResult = tOptional(tObject({}));
scheme.PagePickLocatorParams = tOptional(tObject({}));
scheme.PagePickLocatorResult = tObject({
selector: tString,
});
scheme.PageVideoStartParams = tObject({
size: tOptional(tObject({
width: tInt,
Expand Down
10 changes: 10 additions & 0 deletions packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
import { SdkObject } from '../instrumentation';
import { deserializeURLMatch, urlMatches } from '../../utils/isomorphic/urlMatch';
import { PageAgentDispatcher } from './pageAgentDispatcher';
import { Recorder } from '../recorder';
import { isUnderTest } from '../utils/debug';

import type { Artifact } from '../artifact';
import type { BrowserContext } from '../browserContext';
Expand Down Expand Up @@ -344,6 +346,14 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
await progress.race(this._page.bringToFront());
}

async pickLocator(params: channels.PagePickLocatorParams, progress: Progress): Promise<channels.PagePickLocatorResult> {
if (!this._page.browserContext._browser.options.headful && !isUnderTest())
throw new Error('pickLocator() is only available in headed mode');
const recorder = await Recorder.forContext(this._page.browserContext, { omitCallTracking: true, hideToolbar: true });
const selector = await recorder.pickLocator(progress);
return { selector };
}

async videoStart(params: channels.PageVideoStartParams, progress: Progress): Promise<channels.PageVideoStartResult> {
const artifact = await this._page.screencast.startExplicitVideoRecording(params);
return { artifact: createVideoDispatcher(this.parentScope(), artifact) };
Expand Down
35 changes: 35 additions & 0 deletions packages/playwright-core/src/server/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { buildFullSelector, generateFrameSelector, metadataToCallLog } from './r
import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
import { stringifySelector } from '../utils/isomorphic/selectorParser';
import { ProgressController } from './progress';
import { ManualPromise } from '../utils/isomorphic/manualPromise';

import { RecorderSignalProcessor } from './recorder/recorderSignalProcessor';
import * as rawRecorderSource from './../generated/pollingRecorderSource';
import { eventsHelper, monotonicTime } from './../utils';
Expand All @@ -35,6 +37,7 @@ import type { Language } from './codegen/types';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import type { Point } from '../utils/isomorphic/types';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
import type { Progress } from './progress';
import type * as channels from '@protocol/channels';
import type * as actions from '@recorder/actions';
import type { CallLog, CallLogStatus, ElementInfo, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes';
Expand Down Expand Up @@ -262,6 +265,38 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
this._refreshOverlay();
}

async pickLocator(progress: Progress): Promise<string> {
const selectorPromise = new ManualPromise<string>();
let recorderChangedState = false;
const onElementPicked = (elementInfo: ElementInfo) => {
selectorPromise.resolve(elementInfo.selector);
};
const onModeChanged = () => {
recorderChangedState = true;
selectorPromise.reject(new Error('Locator picking was cancelled'));
};
const onContextClosed = () => {
recorderChangedState = true;
selectorPromise.reject(new Error('Context was closed'));
};
// Register listeners after setMode() to avoid consuming the ModeChanged
// event that fires synchronously from our own setMode('inspecting') call.
this.setMode('inspecting');
const listeners: RegisteredListener[] = [
eventsHelper.addEventListener(this, RecorderEvent.ElementPicked, onElementPicked),
eventsHelper.addEventListener(this, RecorderEvent.ModeChanged, onModeChanged),
eventsHelper.addEventListener(this, RecorderEvent.ContextClosed, onContextClosed),
];
try {
return await progress.race(selectorPromise);
} finally {
// Remove listeners before setMode('none') to avoid triggering onModeChanged.
eventsHelper.removeEventListeners(listeners);
if (!recorderChangedState)
this.setMode('none');
}
}

url(): string | undefined {
const page = this._context.pages()[0];
return page?.mainFrame().url();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['Page.startCSSCoverage', { title: 'Start CSS coverage', group: 'configuration', }],
['Page.stopCSSCoverage', { title: 'Stop CSS coverage', group: 'configuration', }],
['Page.bringToFront', { title: 'Bring to front', }],
['Page.pickLocator', { title: 'Pick locator', }],
['Page.videoStart', { title: 'Start video recording', group: 'configuration', }],
['Page.videoStop', { title: 'Stop video recording', group: 'configuration', }],
['Page.updateSubscription', { internal: true, }],
Expand Down
17 changes: 17 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3904,6 +3904,23 @@ export interface Page {
width?: string|number;
}): Promise<Buffer>;

/**
* Enters pick locator mode where hovering over page elements highlights them and shows the corresponding locator.
* Once the user clicks an element, the mode is deactivated and the
* [Locator](https://playwright.dev/docs/api/class-locator) for the picked element is returned.
*
* **NOTE** This method requires Playwright to be started in a headed mode.
*
* **Usage**
*
* ```js
* const locator = await page.pickLocator();
* console.log(locator);
* ```
*
*/
pickLocator(): Promise<Locator>;

/**
* **NOTE** Use locator-based [locator.press(key[, options])](https://playwright.dev/docs/api/class-locator#locator-press)
* instead. Read more about [locators](https://playwright.dev/docs/locators).
Expand Down
6 changes: 6 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2146,6 +2146,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
startCSSCoverage(params: PageStartCSSCoverageParams, progress?: Progress): Promise<PageStartCSSCoverageResult>;
stopCSSCoverage(params?: PageStopCSSCoverageParams, progress?: Progress): Promise<PageStopCSSCoverageResult>;
bringToFront(params?: PageBringToFrontParams, progress?: Progress): Promise<PageBringToFrontResult>;
pickLocator(params?: PagePickLocatorParams, progress?: Progress): Promise<PagePickLocatorResult>;
videoStart(params: PageVideoStartParams, progress?: Progress): Promise<PageVideoStartResult>;
videoStop(params?: PageVideoStopParams, progress?: Progress): Promise<PageVideoStopResult>;
updateSubscription(params: PageUpdateSubscriptionParams, progress?: Progress): Promise<PageUpdateSubscriptionResult>;
Expand Down Expand Up @@ -2652,6 +2653,11 @@ export type PageStopCSSCoverageResult = {
export type PageBringToFrontParams = {};
export type PageBringToFrontOptions = {};
export type PageBringToFrontResult = void;
export type PagePickLocatorParams = {};
export type PagePickLocatorOptions = {};
export type PagePickLocatorResult = {
selector: string,
};
export type PageVideoStartParams = {
size?: {
width: number,
Expand Down
5 changes: 5 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,11 @@ Page:
bringToFront:
title: Bring to front

pickLocator:
title: Pick locator
returns:
selector: string

videoStart:
title: Start video recording
group: configuration
Expand Down
14 changes: 14 additions & 0 deletions tests/library/inspector/recorder-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,17 @@ test('should disable recorder', async ({ context }) => {
await page.getByRole('button', { name: 'Submit' }).click();
expect(log.action('click')).toHaveLength(2);
});

test('page.pickLocator should return locator for picked element', async ({ page }) => {
await page.setContent(`<button>Submit</button>`);

const scriptReady = page.waitForEvent('console', msg => msg.text() === 'Recorder script ready for test');
const pickPromise = page.pickLocator();
await scriptReady;

const box = await page.getByRole('button', { name: 'Submit' }).boundingBox();
await page.mouse.click(box!.x + box!.width / 2, box!.y + box!.height / 2);

const locator = await pickPromise;
await expect(locator).toHaveText('Submit');
});
Loading