Skip to content

Commit

Permalink
feat(page): Pick best locator for existing element.
Browse files Browse the repository at this point in the history
Improve programmatic API of accessing the "pick locator" functionality
as exposed by the playwright trace viewer/codegenerator.

Fixes microsoft#13900
  • Loading branch information
Germandrummer92 committed Mar 13, 2024
1 parent 914208c commit 34f5539
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 0 deletions.
33 changes: 33 additions & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,39 @@ Gets the full HTML contents of the page, including the doctype.

Get the browser context that the page belongs to.

## method: Page.pickBestLocator
* since: v1.36
- returns: <[Promise<{ locator: string; selector: string}>]>

Get the best locator and selector for the specified selector.

**Usage**

```js
await page.pickBestLocator('#id');
```

```java
page.pickBestLocator("#id");
```

```python async
await page.pickBestLocator("#id")
```

```python sync
page.pickBestLocator("#id")
```

```csharp
await page.PickBestLocatorAsync("#id");
```

Will pick the most stable locator available for the element defined by the selector parameter and return it.

### param: Page.pickBestLocator.selector = %%-input-selector-%%
* since: v1.36

## property: Page.coverage
* since: v1.8
* langs: js
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,10 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
return result.pdf;
}

pickBestLocator(selector: string): Promise<{ locator: string; selector: string }> {
return this._channel.pickBestLocator({ selector });

Check failure on line 779 in packages/playwright-core/src/client/page.ts

View workflow job for this annotation

GitHub Actions / docs & lint

Return statement in a class should await a promise so we are able to extract the whole stack trace when reporting it to e.g. Trace Viewer
}
}

export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
Expand Down
7 changes: 7 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,13 @@ scheme.PageWebSocketEvent = tObject({
scheme.PageWorkerEvent = tObject({
worker: tChannel(['Worker']),
});
scheme.PagePickBestLocatorParams = tObject({
selector: tString,
});
scheme.PagePickBestLocatorResult = tObject({
selector: tString,
locator: tString,
});
scheme.PageSetDefaultNavigationTimeoutNoReplyParams = tObject({
timeout: tOptional(tNumber),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ArtifactDispatcher } from './artifactDispatcher';
import type { Download } from '../download';
import { createGuid, urlMatches } from '../../utils';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { PagePickBestLocatorResult } from '@protocol/channels';

export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel {
_type_EventTarget = true;
Expand Down Expand Up @@ -101,6 +102,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
return this._page;
}

async pickBestLocator(params: channels.PagePickBestLocatorParams, metadata: CallMetadata): Promise<PagePickBestLocatorResult> {
return this._page.pickBestLocator(params.selector);
}

async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.setDefaultNavigationTimeout(params.timeout);
}
Expand Down
15 changes: 15 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3540,6 +3540,21 @@ export interface Page {
width?: string|number;
}): Promise<Buffer>;

/**
* Get the best locator and selector for the specified selector.
*
* **Usage**
*
* ```js
* await page.pickBestLocator('#id');
* ```
*
* Will pick the most stable locator available for the element defined by the selector parameter and return it.
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be
* used.
*/
pickBestLocator(selector: string): Promise<{ locator: string; selector: string}>;

/**
* **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
11 changes: 11 additions & 0 deletions packages/protocol/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1772,6 +1772,7 @@ export interface PageEventTarget {
}
export interface PageChannel extends PageEventTarget, EventTargetChannel {
_type_Page: boolean;
pickBestLocator(params: PagePickBestLocatorParams, metadata?: CallMetadata): Promise<PagePickBestLocatorResult>;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: CallMetadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: CallMetadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
addInitScript(params: PageAddInitScriptParams, metadata?: CallMetadata): Promise<PageAddInitScriptResult>;
Expand Down Expand Up @@ -1843,6 +1844,16 @@ export type PageWebSocketEvent = {
export type PageWorkerEvent = {
worker: WorkerChannel,
};
export type PagePickBestLocatorParams = {
selector: string,
};
export type PagePickBestLocatorOptions = {

};
export type PagePickBestLocatorResult = {
selector: string,
locator: string,
};
export type PageSetDefaultNavigationTimeoutNoReplyParams = {
timeout?: number,
};
Expand Down
6 changes: 6 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,12 @@ Page:
opener: Page?

commands:
pickBestLocator:
parameters:
selector: string
returns:
selector: string
locator: string

setDefaultNavigationTimeoutNoReply:
parameters:
Expand Down
31 changes: 31 additions & 0 deletions tests/page/locator-convenience.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@

import { test as it, expect } from './pageTest';

it.describe('picking the best available locator for an existing element', () => {

const html = '<div><input id="inputId" placeholder="somePlaceholder"/><button id="buttonId">Some Text</button></div>';

const testCases = [
{
originalSelector: '#buttonId',
expectedSelector: 'internal:role=button[name="Some Text"i]',
expectedLocator: "getByRole('button', { name: 'Some Text' })"
},
{
originalSelector: '#inputId',
expectedSelector: 'internal:attr=[placeholder="somePlaceholder"i]',
expectedLocator: "getByPlaceholder('somePlaceholder')"
}];

for (const testCase of testCases) {
it(`should allow selecting the best locator from an existing selector ${testCase.originalSelector}`, async ({ page }) => {
await page.setContent(html);

const locatorReference = await page.pickBestLocator(testCase.originalSelector);

expect(locatorReference.locator).toEqual(testCase.expectedLocator);
expect(locatorReference.selector).toEqual(testCase.expectedSelector);
await page.close();
});
}

});


it('should have a nice preview', async ({ page, server }) => {
await page.goto(`${server.PREFIX}/dom.html`);
const outer = page.locator('#outer');
Expand Down

0 comments on commit 34f5539

Please sign in to comment.