api(frame-locator): introduce frame locators (#10102)

This commit is contained in:
Pavel Feldman 2021-11-08 09:58:24 -08:00 коммит произвёл GitHub
Родитель 7278fcffb8
Коммит 4553d76fce
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
16 изменённых файлов: 567 добавлений и 74 удалений

Просмотреть файл

@ -754,6 +754,42 @@ var contentFrame = await frameElement.ContentFrameAsync();
Console.WriteLine(frame == contentFrame); // -> True
```
## method: Frame.frameLocator
- returns: <[FrameLocator]>
When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements
in that iframe. Following snippet locates element with text "Submit" in the iframe with id `my-frame`,
like `<iframe id="my-frame">`:
```js
const locator = frame.frameLocator('#my-iframe').locator('text=Submit');
await locator.click();
```
```java
Locator locator = frame.frameLocator("#my-iframe").locator("text=Submit");
locator.click();
```
```python async
locator = frame.frame_locator("#my-iframe").locator("text=Submit")
await locator.click()
```
```python sync
locator = frame.frame_locator("#my-iframe").locator("text=Submit")
locator.click()
```
```csharp
var locator = frame.FrameLocator("#my-iframe").Locator("text=Submit");
await locator.ClickAsync();
```
### param: Frame.frameLocator.selector = %%-find-selector-%%
## async method: Frame.getAttribute
- returns: <[null]|[string]>

Просмотреть файл

@ -0,0 +1,44 @@
# class: FrameLocator
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
```js
const locator = page.frameLocator('#my-frame').locator('text=Submit');
await locator.click();
```
```java
Locator locator = page.frameLocator("#my-frame").locator("text=Submit");
locator.click();
```
```python async
locator = page.frame_locator("#my-frame").locator("text=Submit")
await locator.click()
```
```python sync
locator = page.frame_locator("my-frame").locator("text=Submit")
locator.click()
```
```csharp
var locator = page.FrameLocator("#my-frame").Locator("text=Submit");
await locator.ClickAsync();
```
## method: FrameLocator.frameLocator
- returns: <[FrameLocator]>
When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements
in that iframe.
### param: FrameLocator.frameLocator.selector = %%-find-selector-%%
## method: FrameLocator.locator
- returns: <[Locator]>
The method finds an element matching the specified selector in the FrameLocator's subtree.
### param: FrameLocator.locator.selector = %%-find-selector-%%

Просмотреть файл

@ -533,6 +533,41 @@ Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
### option: Locator.focus.timeout = %%-input-timeout-%%
## method: Locator.frameLocator
- returns: <[FrameLocator]>
When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements
in that iframe:
```js
const locator = page.frameLocator('iframe').locator('text=Submit');
await locator.click();
```
```java
Locator locator = page.frameLocator("iframe").locator("text=Submit");
locator.click();
```
```python async
locator = page.frame_locator("iframe").locator("text=Submit")
await locator.click()
```
```python sync
locator = page.frame_locator("text=Submit").locator("text=Submit")
locator.click()
```
```csharp
var locator = page.FrameLocator("iframe").Locator("text=Submit");
await locator.ClickAsync();
```
### param: Locator.frameLocator.selector = %%-find-selector-%%
## async method: Locator.getAttribute
- returns: <[null]|[string]>
@ -641,8 +676,7 @@ Returns locator to the last matching element.
## method: Locator.locator
- returns: <[Locator]>
The method finds an element matching the specified selector in the `Locator`'s subtree. See
[Working with selectors](./selectors.md) for more details.
The method finds an element matching the specified selector in the `Locator`'s subtree.
### param: Locator.locator.selector = %%-find-selector-%%

Просмотреть файл

@ -1843,6 +1843,42 @@ Returns frame with matching URL.
A glob pattern, regex pattern or predicate receiving frame's `url` as a [URL] object.
## method: Page.frameLocator
- returns: <[FrameLocator]>
When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements
in that iframe. Following snippet locates element with text "Submit" in the iframe with id `my-frame`,
like `<iframe id="my-frame">`:
```js
const locator = page.frameLocator('#my-iframe').locator('text=Submit');
await locator.click();
```
```java
Locator locator = page.frameLocator("#my-iframe").locator("text=Submit");
locator.click();
```
```python async
locator = page.frame_locator("#my-iframe").locator("text=Submit")
await locator.click()
```
```python sync
locator = page.frame_locator("#my-iframe").locator("text=Submit")
locator.click()
```
```csharp
var locator = page.FrameLocator("#my-iframe").Locator("text=Submit");
await locator.ClickAsync();
```
### param: Page.frameLocator.selector = %%-find-selector-%%
## method: Page.frames
- returns: <[Array]<[Frame]>>

14
package-lock.json сгенерированный
Просмотреть файл

@ -44,7 +44,7 @@
"ansi-to-html": "^0.7.1",
"babel-loader": "^8.2.2",
"chokidar": "^3.5.0",
"chromedriver": "^95.0.0",
"chromedriver": "^94.0.0",
"commonmark": "^0.29.1",
"concurrently": "^6.2.1",
"copy-webpack-plugin": "^9.0.1",
@ -2537,9 +2537,9 @@
}
},
"node_modules/chromedriver": {
"version": "95.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-95.0.0.tgz",
"integrity": "sha512-HwSg7S0ZZYsHTjULwxFHrrUqEpz1+ljDudJM3eOquvqD5QKnR5pSe/GlBTY9UU2tVFRYz8bEHYC4Y8qxciQiLQ==",
"version": "94.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-94.0.0.tgz",
"integrity": "sha512-x4hK7R7iOyAhdLHJEcOyGBW/oa2kno6AqpHVLd+n3G7c2Vk9XcAXMz84XhNItqykJvTc6E3z/JRIT1eHYH//Eg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@ -11405,9 +11405,9 @@
"dev": true
},
"chromedriver": {
"version": "95.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-95.0.0.tgz",
"integrity": "sha512-HwSg7S0ZZYsHTjULwxFHrrUqEpz1+ljDudJM3eOquvqD5QKnR5pSe/GlBTY9UU2tVFRYz8bEHYC4Y8qxciQiLQ==",
"version": "94.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-94.0.0.tgz",
"integrity": "sha512-x4hK7R7iOyAhdLHJEcOyGBW/oa2kno6AqpHVLd+n3G7c2Vk9XcAXMz84XhNItqykJvTc6E3z/JRIT1eHYH//Eg==",
"dev": true,
"requires": {
"@testim/chrome-version": "^1.0.7",

Просмотреть файл

@ -25,7 +25,7 @@ export { Coverage } from './coverage';
export { Dialog } from './dialog';
export { Download } from './download';
export { Electron, ElectronApplication } from './electron';
export { Locator } from './locator';
export { Locator, FrameLocator } from './locator';
export { ElementHandle } from './elementHandle';
export { FileChooser } from './fileChooser';
export type { Logger } from './types';

Просмотреть файл

@ -18,7 +18,7 @@
import { assert } from '../utils/utils';
import * as channels from '../protocol/channels';
import { ChannelOwner } from './channelOwner';
import { Locator } from './locator';
import { FrameLocator, Locator } from './locator';
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
import { assertMaxArguments, JSHandle, serializeArgument, parseResult } from './jsHandle';
import fs from 'fs';
@ -322,6 +322,10 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
return new Locator(this, selector);
}
frameLocator(selector: string): FrameLocator {
return new FrameLocator(this, selector);
}
async focus(selector: string, options: channels.FrameFocusOptions = {}) {
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
await channel.focus({ selector, ...options });

Просмотреть файл

@ -90,6 +90,10 @@ export class Locator implements api.Locator {
return new Locator(this._frame, this._selector + ' >> ' + selector);
}
frameLocator(selector: string): FrameLocator {
return new FrameLocator(this._frame, this._selector + ' >> ' + selector);
}
async elementHandle(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!;
}
@ -245,3 +249,21 @@ export class Locator implements api.Locator {
return `Locator@${this._selector}`;
}
}
export class FrameLocator implements api.FrameLocator {
private _frame: Frame;
private _selector: string;
constructor(frame: Frame, selector: string) {
this._frame = frame;
this._selector = selector + ' >> control=enter-frame';
}
locator(selector: string): Locator {
return new Locator(this._frame, this._selector + ' >> ' + selector);
}
frameLocator(selector: string): FrameLocator {
return new FrameLocator(this._frame, this._selector + ' >> ' + selector);
}
}

Просмотреть файл

@ -27,7 +27,7 @@ import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
import { Download } from './download';
import { ElementHandle, determineScreenshotType } from './elementHandle';
import { Locator } from './locator';
import { Locator, FrameLocator } from './locator';
import { Worker } from './worker';
import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame';
import { Keyboard, Mouse, Touchscreen } from './input';
@ -543,6 +543,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this.mainFrame().locator(selector);
}
frameLocator(selector: string): FrameLocator {
return this.mainFrame().frameLocator(selector);
}
async focus(selector: string, options?: channels.FrameFocusOptions) {
return this._mainFrame.focus(selector, options);
}

Просмотреть файл

@ -64,9 +64,9 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
let chunkStartIndex = 0;
for (let i = 0; i < selector.parts.length; ++i) {
const part = selector.parts[i];
if (part.name === 'content-frame') {
if (part.name === 'control' && part.body === 'enter-frame') {
if (!chunk.parts.length)
throw new Error('Selector cannot start with "content-frame", select the iframe first');
throw new Error('Selector cannot start with entering frame, select the iframe first');
result.push(chunk);
chunk = { parts: [] };
chunkStartIndex = i + 1;
@ -77,18 +77,20 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
chunk.parts.push(part);
}
if (!chunk.parts.length)
throw new Error(`Selector cannot end with "content-frame", while parsing selector ${selectorText}`);
throw new Error(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);
result.push(chunk);
if (typeof selector.capture === 'number' && typeof result[result.length - 1].capture !== 'number')
throw new Error(`Can not capture the selector before diving into the frame. Only use * after the last "content-frame"`);
throw new Error(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);
return result;
}
export function stringifySelector(selector: string | ParsedSelector): string {
if (typeof selector === 'string')
return selector;
return selector.parts.map((p, i) => `${i === selector.capture ? '*' : ''}${p.name}=${p.source}`).join(' >> ');
return selector.parts.map((p, i) => {
const prefix = p.name === 'css' ? '' : p.name + '=';
return `${i === selector.capture ? '*' : ''}${prefix}${p.source}`;
}).join(' >> ');
}
function parseSelectorString(selector: string): ParsedSelectorStrings {

Просмотреть файл

@ -73,6 +73,11 @@ export type NavigationEvent = {
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;
export type DomTaskBody<T, R, E> = (progress: InjectedScriptProgress, element: E, data: T, elements: Element[]) => R | symbol;
type SelectorInFrame = {
frame: Frame;
info: SelectorInfo;
};
export class FrameManager {
private _page: Page;
private _frames = new Map<string, Frame>();
@ -729,8 +734,10 @@ export class Frame extends SdkObject {
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
return this.retryWithProgress(progress, selector, options, async (frame, info, continuePolling) => {
return this.retryWithProgress(progress, selector, options, async (selectorInFrame, continuePolling) => {
// Be careful, |this| can be different from |frame|.
// We did not pass omitAttached, so it is non-null.
const { frame, info } = selectorInFrame!;
const actualScope = this === frame ? scope : undefined;
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue, actualScope);
const result = actualScope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
@ -965,20 +972,24 @@ export class Frame extends SdkObject {
async retryWithProgress<R>(
progress: Progress,
selector: string,
options: types.StrictOptions & types.TimeoutOptions,
action: (frame: Frame, info: SelectorInfo, continuePolling: symbol) => Promise<R | symbol>,
options: types.StrictOptions & types.TimeoutOptions & { omitAttached?: boolean },
action: (selector: SelectorInFrame | null, continuePolling: symbol) => Promise<R | symbol>,
scope?: dom.ElementHandle): Promise<R> {
const continuePolling = Symbol('continuePolling');
while (progress.isRunning()) {
const pair = await this._resolveFrameForSelector(progress, selector, options, scope);
if (!pair) {
// Missing content frame.
await new Promise(f => setTimeout(f, 100));
continue;
let selectorInFrame: SelectorInFrame | null;
if (options.omitAttached) {
selectorInFrame = await this.resolveFrameForSelectorNoWait(selector, options, scope);
} else {
selectorInFrame = await this._resolveFrameForSelector(progress, selector, options, scope);
if (!selectorInFrame) {
// Missing content frame.
await new Promise(f => setTimeout(f, 100));
continue;
}
}
const { frame, info } = pair;
try {
const result = await action(frame, info, continuePolling);
const result = await action(selectorInFrame, continuePolling);
if (result === continuePolling)
continue;
return result as R;
@ -987,7 +998,7 @@ export class Frame extends SdkObject {
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
throw e;
// If error has happened in the detached inner frame, ignore it, keep polling.
if (frame !== this && frame.isDetached())
if (selectorInFrame?.frame !== this && selectorInFrame?.frame.isDetached())
continue;
throw e;
}
@ -1001,10 +1012,12 @@ export class Frame extends SdkObject {
selector: string,
strict: boolean | undefined,
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
return this.retryWithProgress(progress, selector, { strict }, async (frame, info, continuePolling) => {
return this.retryWithProgress(progress, selector, { strict }, async (selectorInFrame, continuePolling) => {
// We did not pass omitAttached, so selectorInFrame is not null.
const { frame, info } = selectorInFrame!;
// Be careful, |this| can be different from |frame|.
progress.log(`waiting for selector "${selector}"`);
const task = dom.waitForSelectorTask(info, 'attached');
progress.log(`waiting for selector "${selector}"`);
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
const element = handle.asElement() as dom.ElementHandle<Element>;
try {
@ -1210,6 +1223,22 @@ export class Frame extends SdkObject {
const controller = new ProgressController(metadata, this);
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
const mainWorld = options.expression === 'to.have.property';
// List all combinations that are satisfied with the detached node(s).
let omitAttached = false;
if (!options.isNot && options.expression === 'to.be.hidden')
omitAttached = true;
else if (options.isNot && options.expression === 'to.be.visible')
omitAttached = true;
else if (!options.isNot && options.expression === 'to.have.count' && options.expectedNumber === 0)
omitAttached = true;
else if (options.isNot && options.expression === 'to.have.count' && options.expectedNumber !== 0)
omitAttached = true;
else if (!options.isNot && options.expression.endsWith('.array') && options.expectedText!.length === 0)
omitAttached = true;
else if (options.isNot && options.expression.endsWith('.array') && options.expectedText!.length > 0)
omitAttached = true;
return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements) => {
let result: { matches: boolean, received?: any };
@ -1241,7 +1270,7 @@ export class Frame extends SdkObject {
// Reached the expected state!
return result;
}, { ...options, isArray }, { strict: true, querySelectorAll: isArray, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
}, { ...options, isArray }, { strict: true, querySelectorAll: isArray, mainWorld, omitAttached, logScale: true, ...options }).catch(e => {
// Q: Why not throw upon isSessionClosedError(e) as in other places?
// A: We want user to receive a friendly message containing the last intermediate result.
if (js.isJavaScriptErrorInEvaluate(e))
@ -1326,9 +1355,10 @@ export class Frame extends SdkObject {
const callbackText = body.toString();
return controller.run(async progress => {
return this.retryWithProgress(progress, selector, options, async (frame, info) => {
return this.retryWithProgress(progress, selector, options, async selectorInFrame => {
// Be careful, |this| can be different from |frame|.
progress.log(`waiting for selector "${selector}"`);
const { frame, info } = selectorInFrame || { frame: this, info: { parsed: { parts: [{ name: 'control', body: 'return-empty', source: 'control=return-empty' }] }, world: 'utility', strict: !!options.strict } };
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
});
}, this._page._timeoutSettings.timeout(options));
@ -1461,7 +1491,7 @@ export class Frame extends SdkObject {
}, { source, arg });
}
private async _resolveFrameForSelector(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<{ frame: Frame, info: SelectorInfo } | null> {
private async _resolveFrameForSelector(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<SelectorInFrame | null> {
const elementPath: dom.ElementHandle<Element>[] = [];
progress.cleanupWhenAborted(() => {
// Do not await here to avoid being blocked, either by stalled
@ -1476,6 +1506,7 @@ export class Frame extends SdkObject {
for (let i = 0; i < frameChunks.length - 1 && progress.isRunning(); ++i) {
const info = this._page.parseSelector(frameChunks[i], options);
const task = dom.waitForSelectorTask(info, 'attached', false, i === 0 ? scope : undefined);
progress.log(` waiting for frame "${stringifySelector(frameChunks[i])}"`);
const handle = i === 0 && scope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
: await frame._scheduleRerunnableHandleTask(progress, info.world, task);
const element = handle.asElement() as dom.ElementHandle<Element>;
@ -1492,7 +1523,7 @@ export class Frame extends SdkObject {
return { frame, info: this._page.parseSelector(frameChunks[frameChunks.length - 1], options) };
}
async resolveFrameForSelectorNoWait(selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<{ frame: Frame, info: SelectorInfo } | null> {
async resolveFrameForSelectorNoWait(selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<SelectorInFrame | null> {
let frame: Frame | null = this;
const frameChunks = splitSelectorByFrame(selector);

Просмотреть файл

@ -96,6 +96,7 @@ export class InjectedScript {
this._engines.set('css', this._createCSSEngine());
this._engines.set('nth', { queryAll: () => [] });
this._engines.set('visible', { queryAll: () => [] });
this._engines.set('control', this._createControlEngine());
for (const { name, engine } of customEngines)
this._engines.set(name, engine);
@ -263,6 +264,18 @@ export class InjectedScript {
};
}
private _createControlEngine(): SelectorEngineV2 {
return {
queryAll(root: SelectorRoot, body: any) {
if (body === 'enter-frame')
return [];
if (body === 'return-empty')
return [];
throw new Error(`Internal error, unknown control selector ${body}`);
}
};
}
extend(source: string, params: any): any {
const constrFunction = global.eval(`
(() => {

Просмотреть файл

@ -44,7 +44,7 @@ export class Selectors {
'data-testid', 'data-testid:light',
'data-test-id', 'data-test-id:light',
'data-test', 'data-test:light',
'nth', 'visible', 'content-frame'
'nth', 'visible', 'control'
]);
this._builtinEnginesInMainWorld = new Set([
'_react', '_vue',

71
packages/playwright-core/types/types.d.ts поставляемый
Просмотреть файл

@ -2096,6 +2096,20 @@ export interface Page {
url?: string|RegExp|((url: URL) => boolean);
}): null|Frame;
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe. Following snippet locates element with text "Submit" in the iframe with id `my-frame`, like `<iframe
* id="my-frame">`:
*
* ```js
* const locator = page.frameLocator('#my-iframe').locator('text=Submit');
* await locator.click();
* ```
*
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
*/
frameLocator(selector: string): FrameLocator;
/**
* An array of all frames attached to the page.
*/
@ -4866,6 +4880,20 @@ export interface Frame {
*/
frameElement(): Promise<ElementHandle>;
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe. Following snippet locates element with text "Submit" in the iframe with id `my-frame`, like `<iframe
* id="my-frame">`:
*
* ```js
* const locator = frame.frameLocator('#my-iframe').locator('text=Submit');
* await locator.click();
* ```
*
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
*/
frameLocator(selector: string): FrameLocator;
/**
* Returns element attribute value.
* @param selector A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
@ -8883,6 +8911,19 @@ export interface Locator {
timeout?: number;
}): Promise<void>;
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe:
*
* ```js
* const locator = page.frameLocator('iframe').locator('text=Submit');
* await locator.click();
* ```
*
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
*/
frameLocator(selector: string): FrameLocator;
/**
* Returns element attribute value.
* @param name Attribute name to get the value for.
@ -9081,8 +9122,7 @@ export interface Locator {
last(): Locator;
/**
* The method finds an element matching the specified selector in the `Locator`'s subtree. See
* [Working with selectors](https://playwright.dev/docs/selectors) for more details.
* The method finds an element matching the specified selector in the `Locator`'s subtree.
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
*/
locator(selector: string): Locator;
@ -13430,6 +13470,33 @@ export interface FileChooser {
}): Promise<void>;
}
/**
* FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe`
* and locate elements in that iframe. FrameLocator can be created with either
* [page.frameLocator(selector)](https://playwright.dev/docs/api/class-page#page-frame-locator) or
* [locator.frameLocator(selector)](https://playwright.dev/docs/api/class-locator#locator-frame-locator) method.
*
* ```js
* const locator = page.frameLocator('#my-frame').locator('text=Submit');
* await locator.click();
* ```
*
*/
export interface FrameLocator {
/**
* When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in
* that iframe.
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
*/
frameLocator(selector: string): FrameLocator;
/**
* The method finds an element matching the specified selector in the FrameLocator's subtree.
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
*/
locator(selector: string): Locator;
}
/**
* Keyboard provides an api for managing a virtual keyboard. The high level api is
* [keyboard.type(text[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-type), which takes raw

Просмотреть файл

@ -0,0 +1,200 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page } from 'playwright-core';
import { test as it, expect } from './pageTest';
async function routeIframe(page: Page) {
await page.route('**/empty.html', route => {
route.fulfill({
body: '<iframe src="iframe.html"></iframe>',
contentType: 'text/html'
}).catch(() => {});
});
await page.route('**/iframe.html', route => {
route.fulfill({
body: `
<html>
<div>
<button>Hello iframe</button>
<iframe src="iframe-2.html"></iframe>
</div>
<span>1</span>
<span>2</span>
</html>`,
contentType: 'text/html'
}).catch(() => {});
});
await page.route('**/iframe-2.html', route => {
route.fulfill({
body: '<html><button>Hello nested iframe</button></html>',
contentType: 'text/html'
}).catch(() => {});
});
}
it('should work for iframe', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.frameLocator('iframe').locator('button');
await button.waitFor();
expect(await button.innerText()).toBe('Hello iframe');
await expect(button).toHaveText('Hello iframe');
await button.click();
});
it('should work for nested iframe', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.frameLocator('iframe').frameLocator('iframe').locator('button');
await button.waitFor();
expect(await button.innerText()).toBe('Hello nested iframe');
await expect(button).toHaveText('Hello nested iframe');
await button.click();
});
it('should work for $ and $$', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const locator = page.frameLocator('iframe').locator('button');
await expect(locator).toHaveText('Hello iframe');
const spans = page.frameLocator('iframe').locator('span');
await expect(spans).toHaveCount(2);
});
it('should wait for frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
const error = await page.frameLocator('iframe').locator('span').click({ timeout: 300 }).catch(e => e);
expect(error.message).toContain('waiting for frame "iframe"');
});
it('should wait for frame 2', async ({ page, server }) => {
await routeIframe(page);
setTimeout(() => page.goto(server.EMPTY_PAGE).catch(() => {}), 300);
await page.frameLocator('iframe').locator('button').click();
});
it('should wait for frame to go', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
setTimeout(() => page.$eval('iframe', e => e.remove()).catch(() => {}), 300);
await expect(page.frameLocator('iframe').locator('button')).toBeHidden();
});
it('should not wait for frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await expect(page.frameLocator('iframe').locator('span')).toBeHidden();
});
it('should not wait for frame 2', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await expect(page.frameLocator('iframe').locator('span')).not.toBeVisible();
});
it('should not wait for frame 3', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await expect(page.frameLocator('iframe').locator('span')).toHaveCount(0);
});
it('should click in lazy iframe', async ({ page, server }) => {
await page.route('**/iframe.html', route => {
route.fulfill({
body: '<html><button>Hello iframe</button></html>',
contentType: 'text/html'
}).catch(() => {});
});
// empty pge
await page.goto(server.EMPTY_PAGE);
// add blank iframe
setTimeout(() => {
page.evaluate(() => {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
});
// navigate iframe
setTimeout(() => {
page.evaluate(() => document.querySelector('iframe').src = 'iframe.html');
}, 500);
}, 500);
// Click in iframe
const button = page.frameLocator('iframe').locator('button');
const [, text] = await Promise.all([
button.click(),
button.innerText(),
expect(button).toHaveText('Hello iframe')
]);
expect(text).toBe('Hello iframe');
});
it('waitFor should survive frame reattach', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.frameLocator('iframe').locator('button:has-text("Hello nested iframe")');
const promise = button.waitFor();
await page.locator('iframe').evaluate(e => e.remove());
await page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.src = 'iframe-2.html';
document.body.appendChild(iframe);
});
await promise;
});
it('click should survive frame reattach', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.frameLocator('iframe').locator('button:has-text("Hello nested iframe")');
const promise = button.click();
await page.locator('iframe').evaluate(e => e.remove());
await page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.src = 'iframe-2.html';
document.body.appendChild(iframe);
});
await promise;
});
it('click should survive iframe navigation', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.frameLocator('iframe').locator('button:has-text("Hello nested iframe")');
const promise = button.click();
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
await promise;
});
it('should non work for non-frame', async ({ page, server }) => {
await routeIframe(page);
await page.setContent('<div></div>');
const button = page.frameLocator('div').locator('button');
const error = await button.waitFor().catch(e => e);
expect(error.message).toContain('<div></div>');
expect(error.message).toContain('<iframe> was expected');
});
it('locator.frameLocator should work for iframe', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('body').frameLocator('iframe').locator('button');
await button.waitFor();
expect(await button.innerText()).toBe('Hello iframe');
await expect(button).toHaveText('Hello iframe');
await button.click();
});

Просмотреть файл

@ -49,7 +49,7 @@ async function routeIframe(page: Page) {
it('should work for iframe', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('iframe >> content-frame=true >> button');
const button = page.locator('iframe >> control=enter-frame >> button');
await button.waitFor();
expect(await button.innerText()).toBe('Hello iframe');
await expect(button).toHaveText('Hello iframe');
@ -60,7 +60,7 @@ it('should work for iframe (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const button = await body.waitForSelector('iframe >> content-frame=true >> button');
const button = await body.waitForSelector('iframe >> control=enter-frame >> button');
expect(await button.innerText()).toBe('Hello iframe');
expect(await button.textContent()).toBe('Hello iframe');
await button.click();
@ -69,7 +69,7 @@ it('should work for iframe (handle)', async ({ page, server }) => {
it('should work for nested iframe', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('iframe >> content-frame=true >> iframe >> content-frame=true >> button');
const button = page.locator('iframe >> control=enter-frame >> iframe >> control=enter-frame >> button');
await button.waitFor();
expect(await button.innerText()).toBe('Hello nested iframe');
await expect(button).toHaveText('Hello nested iframe');
@ -80,7 +80,7 @@ it('should work for nested iframe (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const button = await body.waitForSelector('iframe >> content-frame=true >> iframe >> content-frame=true >> button');
const button = await body.waitForSelector('iframe >> control=enter-frame >> iframe >> control=enter-frame >> button');
expect(await button.innerText()).toBe('Hello nested iframe');
expect(await button.textContent()).toBe('Hello nested iframe');
await button.click();
@ -89,35 +89,35 @@ it('should work for nested iframe (handle)', async ({ page, server }) => {
it('should work for $ and $$', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const element = await page.$('iframe >> content-frame=true >> button');
const element = await page.$('iframe >> control=enter-frame >> button');
expect(await element.textContent()).toBe('Hello iframe');
const elements = await page.$$('iframe >> content-frame=true >> span');
const elements = await page.$$('iframe >> control=enter-frame >> span');
expect(elements).toHaveLength(2);
});
it('$ should not wait for frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
expect(await page.$('iframe >> content-frame=true >> canvas')).toBeFalsy();
expect(await page.$('iframe >> control=enter-frame >> canvas')).toBeFalsy();
const body = await page.$('body');
expect(await body.$('iframe >> content-frame=true >> canvas')).toBeFalsy();
expect(await body.$('iframe >> control=enter-frame >> canvas')).toBeFalsy();
});
it('$$ should not wait for frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
expect(await page.$$('iframe >> content-frame=true >> canvas')).toHaveLength(0);
expect(await page.$$('iframe >> control=enter-frame >> canvas')).toHaveLength(0);
const body = await page.$('body');
expect(await body.$$('iframe >> content-frame=true >> canvas')).toHaveLength(0);
expect(await body.$$('iframe >> control=enter-frame >> canvas')).toHaveLength(0);
});
it('$eval should throw for missing frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
{
const error = await page.$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
const error = await page.$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find element matching selector');
}
{
const body = await page.$('body');
const error = await body.$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
const error = await body.$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find element matching selector');
}
});
@ -125,12 +125,12 @@ it('$eval should throw for missing frame', async ({ page, server }) => {
it('$$eval should throw for missing frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
{
const error = await page.$$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
const error = await page.$$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find frame for selector');
}
{
const body = await page.$('body');
const error = await body.$$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
const error = await body.$$eval('iframe >> control=enter-frame >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find frame for selector');
}
});
@ -139,16 +139,16 @@ it('should work for $ and $$ (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const element = await body.$('iframe >> content-frame=true >> button');
const element = await body.$('iframe >> control=enter-frame >> button');
expect(await element.textContent()).toBe('Hello iframe');
const elements = await body.$$('iframe >> content-frame=true >> span');
const elements = await body.$$('iframe >> control=enter-frame >> span');
expect(elements).toHaveLength(2);
});
it('should work for $eval', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const value = await page.$eval('iframe >> content-frame=true >> button', b => b.nodeName);
const value = await page.$eval('iframe >> control=enter-frame >> button', b => b.nodeName);
expect(value).toBe('BUTTON');
});
@ -156,14 +156,14 @@ it('should work for $eval (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const value = await body.$eval('iframe >> content-frame=true >> button', b => b.nodeName);
const value = await body.$eval('iframe >> control=enter-frame >> button', b => b.nodeName);
expect(value).toBe('BUTTON');
});
it('should work for $$eval', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const value = await page.$$eval('iframe >> content-frame=true >> span', ss => ss.map(s => s.textContent));
const value = await page.$$eval('iframe >> control=enter-frame >> span', ss => ss.map(s => s.textContent));
expect(value).toEqual(['1', '2']);
});
@ -171,38 +171,38 @@ it('should work for $$eval (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const value = await body.$$eval('iframe >> content-frame=true >> span', ss => ss.map(s => s.textContent));
const value = await body.$$eval('iframe >> control=enter-frame >> span', ss => ss.map(s => s.textContent));
expect(value).toEqual(['1', '2']);
});
it('should not allow dangling content-frame', async ({ page, server }) => {
it('should not allow dangling enter-frame', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('iframe >> content-frame=true');
const button = page.locator('iframe >> control=enter-frame');
const error = await button.click().catch(e => e);
expect(error.message).toContain('Selector cannot end with');
expect(error.message).toContain('iframe >> content-frame=true');
expect(error.message).toContain('iframe >> control=enter-frame');
});
it('should not allow leading content-frame', async ({ page, server }) => {
it('should not allow leading enter-frame', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const error = await page.waitForSelector('content-frame=true >> button').catch(e => e);
const error = await page.waitForSelector('control=enter-frame >> button').catch(e => e);
expect(error.message).toContain('Selector cannot start with');
});
it('should not allow capturing before content-frame', async ({ page, server }) => {
it('should not allow capturing before enter-frame', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('*css=iframe >> content-frame=true >> div');
const button = page.locator('*css=iframe >> control=enter-frame >> div');
const error = await await button.click().catch(e => e);
expect(error.message).toContain('Can not capture the selector before diving into the frame');
});
it('should capture after the content-frame', async ({ page, server }) => {
it('should capture after the enter-frame', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const div = page.locator('iframe >> content-frame=true >> *css=div >> button');
const div = page.locator('iframe >> control=enter-frame >> *css=div >> button');
expect(await div.innerHTML()).toContain('<button>');
});
@ -230,7 +230,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
}, 500);
// Click in iframe
const button = page.locator('iframe >> content-frame=true >> button');
const button = page.locator('iframe >> control=enter-frame >> button');
const [, text] = await Promise.all([
button.click(),
button.innerText(),
@ -242,7 +242,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
it('waitFor should survive frame reattach', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
const button = page.locator('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
const promise = button.waitFor();
await page.locator('iframe').evaluate(e => e.remove());
await page.evaluate(() => {
@ -257,7 +257,7 @@ it('waitForSelector should survive frame reattach (handle)', async ({ page, serv
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const promise = body.waitForSelector('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
const promise = body.waitForSelector('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
await page.locator('iframe').evaluate(e => e.remove());
await page.evaluate(() => {
const iframe = document.createElement('iframe');
@ -271,7 +271,7 @@ it('waitForSelector should survive iframe navigation (handle)', async ({ page, s
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const promise = body.waitForSelector('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
const promise = body.waitForSelector('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
await promise;
});
@ -279,7 +279,7 @@ it('waitForSelector should survive iframe navigation (handle)', async ({ page, s
it('click should survive frame reattach', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
const button = page.locator('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
const promise = button.click();
await page.locator('iframe').evaluate(e => e.remove());
await page.evaluate(() => {
@ -293,7 +293,7 @@ it('click should survive frame reattach', async ({ page, server }) => {
it('click should survive iframe navigation', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const button = page.locator('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
const button = page.locator('iframe >> control=enter-frame >> button:has-text("Hello nested iframe")');
const promise = button.click();
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
await promise;
@ -322,7 +322,7 @@ it('should fail if element removed while waiting on element handle', async ({ pa
it('should non work for non-frame', async ({ page, server }) => {
await routeIframe(page);
await page.setContent('<div></div>');
const button = page.locator('div >> content-frame=true >> button');
const button = page.locator('div >> control=enter-frame >> button');
const error = await button.waitFor().catch(e => e);
expect(error.message).toContain('<div></div>');
expect(error.message).toContain('<iframe> was expected');