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
|
||||
```
|
||||
|
||||
|
||||
## 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]>>
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
Загрузка…
Ссылка в новой задаче