api(frame-locator): introduce frame locators (#10102)
This commit is contained in:
Родитель
7278fcffb8
Коммит
4553d76fce
|
@ -754,6 +754,42 @@ var contentFrame = await frameElement.ContentFrameAsync();
|
||||||
Console.WriteLine(frame == contentFrame); // -> True
|
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
|
## async method: Frame.getAttribute
|
||||||
- returns: <[null]|[string]>
|
- 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-%%
|
### 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
|
## async method: Locator.getAttribute
|
||||||
- returns: <[null]|[string]>
|
- returns: <[null]|[string]>
|
||||||
|
|
||||||
|
@ -641,8 +676,7 @@ Returns locator to the last matching element.
|
||||||
## method: Locator.locator
|
## method: Locator.locator
|
||||||
- returns: <[Locator]>
|
- returns: <[Locator]>
|
||||||
|
|
||||||
The method finds an element matching the specified selector in the `Locator`'s subtree. See
|
The method finds an element matching the specified selector in the `Locator`'s subtree.
|
||||||
[Working with selectors](./selectors.md) for more details.
|
|
||||||
|
|
||||||
### param: Locator.locator.selector = %%-find-selector-%%
|
### 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.
|
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
|
## method: Page.frames
|
||||||
- returns: <[Array]<[Frame]>>
|
- returns: <[Array]<[Frame]>>
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
"ansi-to-html": "^0.7.1",
|
"ansi-to-html": "^0.7.1",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"chokidar": "^3.5.0",
|
"chokidar": "^3.5.0",
|
||||||
"chromedriver": "^95.0.0",
|
"chromedriver": "^94.0.0",
|
||||||
"commonmark": "^0.29.1",
|
"commonmark": "^0.29.1",
|
||||||
"concurrently": "^6.2.1",
|
"concurrently": "^6.2.1",
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
"copy-webpack-plugin": "^9.0.1",
|
||||||
|
@ -2537,9 +2537,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "95.0.0",
|
"version": "94.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-95.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-94.0.0.tgz",
|
||||||
"integrity": "sha512-HwSg7S0ZZYsHTjULwxFHrrUqEpz1+ljDudJM3eOquvqD5QKnR5pSe/GlBTY9UU2tVFRYz8bEHYC4Y8qxciQiLQ==",
|
"integrity": "sha512-x4hK7R7iOyAhdLHJEcOyGBW/oa2kno6AqpHVLd+n3G7c2Vk9XcAXMz84XhNItqykJvTc6E3z/JRIT1eHYH//Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -11405,9 +11405,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"chromedriver": {
|
"chromedriver": {
|
||||||
"version": "95.0.0",
|
"version": "94.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-95.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-94.0.0.tgz",
|
||||||
"integrity": "sha512-HwSg7S0ZZYsHTjULwxFHrrUqEpz1+ljDudJM3eOquvqD5QKnR5pSe/GlBTY9UU2tVFRYz8bEHYC4Y8qxciQiLQ==",
|
"integrity": "sha512-x4hK7R7iOyAhdLHJEcOyGBW/oa2kno6AqpHVLd+n3G7c2Vk9XcAXMz84XhNItqykJvTc6E3z/JRIT1eHYH//Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@testim/chrome-version": "^1.0.7",
|
"@testim/chrome-version": "^1.0.7",
|
||||||
|
|
|
@ -25,7 +25,7 @@ export { Coverage } from './coverage';
|
||||||
export { Dialog } from './dialog';
|
export { Dialog } from './dialog';
|
||||||
export { Download } from './download';
|
export { Download } from './download';
|
||||||
export { Electron, ElectronApplication } from './electron';
|
export { Electron, ElectronApplication } from './electron';
|
||||||
export { Locator } from './locator';
|
export { Locator, FrameLocator } from './locator';
|
||||||
export { ElementHandle } from './elementHandle';
|
export { ElementHandle } from './elementHandle';
|
||||||
export { FileChooser } from './fileChooser';
|
export { FileChooser } from './fileChooser';
|
||||||
export type { Logger } from './types';
|
export type { Logger } from './types';
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import { assert } from '../utils/utils';
|
import { assert } from '../utils/utils';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import { Locator } from './locator';
|
import { FrameLocator, Locator } from './locator';
|
||||||
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
|
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
|
||||||
import { assertMaxArguments, JSHandle, serializeArgument, parseResult } from './jsHandle';
|
import { assertMaxArguments, JSHandle, serializeArgument, parseResult } from './jsHandle';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
@ -322,6 +322,10 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
|
||||||
return new Locator(this, selector);
|
return new Locator(this, selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frameLocator(selector: string): FrameLocator {
|
||||||
|
return new FrameLocator(this, selector);
|
||||||
|
}
|
||||||
|
|
||||||
async focus(selector: string, options: channels.FrameFocusOptions = {}) {
|
async focus(selector: string, options: channels.FrameFocusOptions = {}) {
|
||||||
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
|
return this._wrapApiCall(async (channel: channels.FrameChannel) => {
|
||||||
await channel.focus({ selector, ...options });
|
await channel.focus({ selector, ...options });
|
||||||
|
|
|
@ -90,6 +90,10 @@ export class Locator implements api.Locator {
|
||||||
return new Locator(this._frame, this._selector + ' >> ' + selector);
|
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>> {
|
async elementHandle(options?: TimeoutOptions): Promise<ElementHandle<SVGElement | HTMLElement>> {
|
||||||
return await this._frame.waitForSelector(this._selector, { strict: true, state: 'attached', ...options })!;
|
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}`;
|
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 { Dialog } from './dialog';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
import { ElementHandle, determineScreenshotType } from './elementHandle';
|
import { ElementHandle, determineScreenshotType } from './elementHandle';
|
||||||
import { Locator } from './locator';
|
import { Locator, FrameLocator } from './locator';
|
||||||
import { Worker } from './worker';
|
import { Worker } from './worker';
|
||||||
import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame';
|
import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame';
|
||||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||||
|
@ -543,6 +543,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
return this.mainFrame().locator(selector);
|
return this.mainFrame().locator(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frameLocator(selector: string): FrameLocator {
|
||||||
|
return this.mainFrame().frameLocator(selector);
|
||||||
|
}
|
||||||
|
|
||||||
async focus(selector: string, options?: channels.FrameFocusOptions) {
|
async focus(selector: string, options?: channels.FrameFocusOptions) {
|
||||||
return this._mainFrame.focus(selector, options);
|
return this._mainFrame.focus(selector, options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,9 +64,9 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
|
||||||
let chunkStartIndex = 0;
|
let chunkStartIndex = 0;
|
||||||
for (let i = 0; i < selector.parts.length; ++i) {
|
for (let i = 0; i < selector.parts.length; ++i) {
|
||||||
const part = selector.parts[i];
|
const part = selector.parts[i];
|
||||||
if (part.name === 'content-frame') {
|
if (part.name === 'control' && part.body === 'enter-frame') {
|
||||||
if (!chunk.parts.length)
|
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);
|
result.push(chunk);
|
||||||
chunk = { parts: [] };
|
chunk = { parts: [] };
|
||||||
chunkStartIndex = i + 1;
|
chunkStartIndex = i + 1;
|
||||||
|
@ -77,18 +77,20 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
|
||||||
chunk.parts.push(part);
|
chunk.parts.push(part);
|
||||||
}
|
}
|
||||||
if (!chunk.parts.length)
|
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);
|
result.push(chunk);
|
||||||
if (typeof selector.capture === 'number' && typeof result[result.length - 1].capture !== 'number')
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function stringifySelector(selector: string | ParsedSelector): string {
|
export function stringifySelector(selector: string | ParsedSelector): string {
|
||||||
if (typeof selector === 'string')
|
if (typeof selector === 'string')
|
||||||
return selector;
|
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 {
|
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 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;
|
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 {
|
export class FrameManager {
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _frames = new Map<string, Frame>();
|
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)`);
|
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
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|.
|
// 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 actualScope = this === frame ? scope : undefined;
|
||||||
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue, actualScope);
|
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue, actualScope);
|
||||||
const result = actualScope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
|
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>(
|
async retryWithProgress<R>(
|
||||||
progress: Progress,
|
progress: Progress,
|
||||||
selector: string,
|
selector: string,
|
||||||
options: types.StrictOptions & types.TimeoutOptions,
|
options: types.StrictOptions & types.TimeoutOptions & { omitAttached?: boolean },
|
||||||
action: (frame: Frame, info: SelectorInfo, continuePolling: symbol) => Promise<R | symbol>,
|
action: (selector: SelectorInFrame | null, continuePolling: symbol) => Promise<R | symbol>,
|
||||||
scope?: dom.ElementHandle): Promise<R> {
|
scope?: dom.ElementHandle): Promise<R> {
|
||||||
const continuePolling = Symbol('continuePolling');
|
const continuePolling = Symbol('continuePolling');
|
||||||
while (progress.isRunning()) {
|
while (progress.isRunning()) {
|
||||||
const pair = await this._resolveFrameForSelector(progress, selector, options, scope);
|
let selectorInFrame: SelectorInFrame | null;
|
||||||
if (!pair) {
|
if (options.omitAttached) {
|
||||||
// Missing content frame.
|
selectorInFrame = await this.resolveFrameForSelectorNoWait(selector, options, scope);
|
||||||
await new Promise(f => setTimeout(f, 100));
|
} else {
|
||||||
continue;
|
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 {
|
try {
|
||||||
const result = await action(frame, info, continuePolling);
|
const result = await action(selectorInFrame, continuePolling);
|
||||||
if (result === continuePolling)
|
if (result === continuePolling)
|
||||||
continue;
|
continue;
|
||||||
return result as R;
|
return result as R;
|
||||||
|
@ -987,7 +998,7 @@ export class Frame extends SdkObject {
|
||||||
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
|
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
|
||||||
throw e;
|
throw e;
|
||||||
// If error has happened in the detached inner frame, ignore it, keep polling.
|
// 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;
|
continue;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -1001,10 +1012,12 @@ export class Frame extends SdkObject {
|
||||||
selector: string,
|
selector: string,
|
||||||
strict: boolean | undefined,
|
strict: boolean | undefined,
|
||||||
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
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|.
|
// Be careful, |this| can be different from |frame|.
|
||||||
progress.log(`waiting for selector "${selector}"`);
|
|
||||||
const task = dom.waitForSelectorTask(info, 'attached');
|
const task = dom.waitForSelectorTask(info, 'attached');
|
||||||
|
progress.log(`waiting for selector "${selector}"`);
|
||||||
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||||
try {
|
try {
|
||||||
|
@ -1210,6 +1223,22 @@ export class Frame extends SdkObject {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
||||||
const mainWorld = options.expression === 'to.have.property';
|
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) => {
|
return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, options, elements) => {
|
||||||
let result: { matches: boolean, received?: any };
|
let result: { matches: boolean, received?: any };
|
||||||
|
|
||||||
|
@ -1241,7 +1270,7 @@ export class Frame extends SdkObject {
|
||||||
|
|
||||||
// Reached the expected state!
|
// Reached the expected state!
|
||||||
return result;
|
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?
|
// 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.
|
// A: We want user to receive a friendly message containing the last intermediate result.
|
||||||
if (js.isJavaScriptErrorInEvaluate(e))
|
if (js.isJavaScriptErrorInEvaluate(e))
|
||||||
|
@ -1326,9 +1355,10 @@ export class Frame extends SdkObject {
|
||||||
const callbackText = body.toString();
|
const callbackText = body.toString();
|
||||||
|
|
||||||
return controller.run(async progress => {
|
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|.
|
// Be careful, |this| can be different from |frame|.
|
||||||
progress.log(`waiting for selector "${selector}"`);
|
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);
|
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
|
||||||
});
|
});
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
@ -1461,7 +1491,7 @@ export class Frame extends SdkObject {
|
||||||
}, { source, arg });
|
}, { 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>[] = [];
|
const elementPath: dom.ElementHandle<Element>[] = [];
|
||||||
progress.cleanupWhenAborted(() => {
|
progress.cleanupWhenAborted(() => {
|
||||||
// Do not await here to avoid being blocked, either by stalled
|
// 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) {
|
for (let i = 0; i < frameChunks.length - 1 && progress.isRunning(); ++i) {
|
||||||
const info = this._page.parseSelector(frameChunks[i], options);
|
const info = this._page.parseSelector(frameChunks[i], options);
|
||||||
const task = dom.waitForSelectorTask(info, 'attached', false, i === 0 ? scope : undefined);
|
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)
|
const handle = i === 0 && scope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
|
||||||
: await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
: await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
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) };
|
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;
|
let frame: Frame | null = this;
|
||||||
const frameChunks = splitSelectorByFrame(selector);
|
const frameChunks = splitSelectorByFrame(selector);
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ export class InjectedScript {
|
||||||
this._engines.set('css', this._createCSSEngine());
|
this._engines.set('css', this._createCSSEngine());
|
||||||
this._engines.set('nth', { queryAll: () => [] });
|
this._engines.set('nth', { queryAll: () => [] });
|
||||||
this._engines.set('visible', { queryAll: () => [] });
|
this._engines.set('visible', { queryAll: () => [] });
|
||||||
|
this._engines.set('control', this._createControlEngine());
|
||||||
|
|
||||||
for (const { name, engine } of customEngines)
|
for (const { name, engine } of customEngines)
|
||||||
this._engines.set(name, engine);
|
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 {
|
extend(source: string, params: any): any {
|
||||||
const constrFunction = global.eval(`
|
const constrFunction = global.eval(`
|
||||||
(() => {
|
(() => {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export class Selectors {
|
||||||
'data-testid', 'data-testid:light',
|
'data-testid', 'data-testid:light',
|
||||||
'data-test-id', 'data-test-id:light',
|
'data-test-id', 'data-test-id:light',
|
||||||
'data-test', 'data-test:light',
|
'data-test', 'data-test:light',
|
||||||
'nth', 'visible', 'content-frame'
|
'nth', 'visible', 'control'
|
||||||
]);
|
]);
|
||||||
this._builtinEnginesInMainWorld = new Set([
|
this._builtinEnginesInMainWorld = new Set([
|
||||||
'_react', '_vue',
|
'_react', '_vue',
|
||||||
|
|
|
@ -2096,6 +2096,20 @@ export interface Page {
|
||||||
url?: string|RegExp|((url: URL) => boolean);
|
url?: string|RegExp|((url: URL) => boolean);
|
||||||
}): null|Frame;
|
}): 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.
|
* An array of all frames attached to the page.
|
||||||
*/
|
*/
|
||||||
|
@ -4866,6 +4880,20 @@ export interface Frame {
|
||||||
*/
|
*/
|
||||||
frameElement(): Promise<ElementHandle>;
|
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.
|
* 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.
|
* @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;
|
timeout?: number;
|
||||||
}): Promise<void>;
|
}): 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.
|
* Returns element attribute value.
|
||||||
* @param name Attribute name to get the value for.
|
* @param name Attribute name to get the value for.
|
||||||
|
@ -9081,8 +9122,7 @@ export interface Locator {
|
||||||
last(): Locator;
|
last(): Locator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The method finds an element matching the specified selector in the `Locator`'s subtree. See
|
* The method finds an element matching the specified selector in the `Locator`'s subtree.
|
||||||
* [Working with selectors](https://playwright.dev/docs/selectors) for more details.
|
|
||||||
* @param selector A selector to use when resolving DOM element. See [working with selectors](https://playwright.dev/docs/selectors) for more details.
|
* @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;
|
locator(selector: string): Locator;
|
||||||
|
@ -13430,6 +13470,33 @@ export interface FileChooser {
|
||||||
}): Promise<void>;
|
}): 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 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
|
* [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 }) => {
|
it('should work for iframe', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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();
|
await button.waitFor();
|
||||||
expect(await button.innerText()).toBe('Hello iframe');
|
expect(await button.innerText()).toBe('Hello iframe');
|
||||||
await expect(button).toHaveText('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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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.innerText()).toBe('Hello iframe');
|
||||||
expect(await button.textContent()).toBe('Hello iframe');
|
expect(await button.textContent()).toBe('Hello iframe');
|
||||||
await button.click();
|
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 }) => {
|
it('should work for nested iframe', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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();
|
await button.waitFor();
|
||||||
expect(await button.innerText()).toBe('Hello nested iframe');
|
expect(await button.innerText()).toBe('Hello nested iframe');
|
||||||
await expect(button).toHaveText('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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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.innerText()).toBe('Hello nested iframe');
|
||||||
expect(await button.textContent()).toBe('Hello nested iframe');
|
expect(await button.textContent()).toBe('Hello nested iframe');
|
||||||
await button.click();
|
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 }) => {
|
it('should work for $ and $$', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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');
|
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);
|
expect(elements).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$ should not wait for frame', async ({ page, server }) => {
|
it('$ should not wait for frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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');
|
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 }) => {
|
it('$$ should not wait for frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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');
|
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 }) => {
|
it('$eval should throw for missing frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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');
|
expect(error.message).toContain('Error: failed to find element matching selector');
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const body = await page.$('body');
|
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');
|
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 }) => {
|
it('$$eval should throw for missing frame', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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');
|
expect(error.message).toContain('Error: failed to find frame for selector');
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const body = await page.$('body');
|
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');
|
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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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');
|
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);
|
expect(elements).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for $eval', async ({ page, server }) => {
|
it('should work for $eval', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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');
|
expect(value).toBe('BUTTON');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -156,14 +156,14 @@ it('should work for $eval (handle)', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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');
|
expect(value).toBe('BUTTON');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work for $$eval', async ({ page, server }) => {
|
it('should work for $$eval', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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']);
|
expect(value).toEqual(['1', '2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -171,38 +171,38 @@ it('should work for $$eval (handle)', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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']);
|
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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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);
|
const error = await button.click().catch(e => e);
|
||||||
expect(error.message).toContain('Selector cannot end with');
|
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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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');
|
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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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);
|
const error = await await button.click().catch(e => e);
|
||||||
expect(error.message).toContain('Can not capture the selector before diving into the frame');
|
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 routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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>');
|
expect(await div.innerHTML()).toContain('<button>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Click in iframe
|
// 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([
|
const [, text] = await Promise.all([
|
||||||
button.click(),
|
button.click(),
|
||||||
button.innerText(),
|
button.innerText(),
|
||||||
|
@ -242,7 +242,7 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
||||||
it('waitFor should survive frame reattach', async ({ page, server }) => {
|
it('waitFor should survive frame reattach', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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();
|
const promise = button.waitFor();
|
||||||
await page.locator('iframe').evaluate(e => e.remove());
|
await page.locator('iframe').evaluate(e => e.remove());
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
@ -257,7 +257,7 @@ it('waitForSelector should survive frame reattach (handle)', async ({ page, serv
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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.locator('iframe').evaluate(e => e.remove());
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
|
@ -271,7 +271,7 @@ it('waitForSelector should survive iframe navigation (handle)', async ({ page, s
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const body = await page.$('body');
|
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');
|
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
||||||
await promise;
|
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 }) => {
|
it('click should survive frame reattach', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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();
|
const promise = button.click();
|
||||||
await page.locator('iframe').evaluate(e => e.remove());
|
await page.locator('iframe').evaluate(e => e.remove());
|
||||||
await page.evaluate(() => {
|
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 }) => {
|
it('click should survive iframe navigation', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.goto(server.EMPTY_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();
|
const promise = button.click();
|
||||||
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
||||||
await promise;
|
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 }) => {
|
it('should non work for non-frame', async ({ page, server }) => {
|
||||||
await routeIframe(page);
|
await routeIframe(page);
|
||||||
await page.setContent('<div></div>');
|
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);
|
const error = await button.waitFor().catch(e => e);
|
||||||
expect(error.message).toContain('<div></div>');
|
expect(error.message).toContain('<div></div>');
|
||||||
expect(error.message).toContain('<iframe> was expected');
|
expect(error.message).toContain('<iframe> was expected');
|
||||||
|
|
Загрузка…
Ссылка в новой задаче