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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/src/api/class-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -1443,16 +1443,16 @@ Parent frame, if any. Detached frames and main frames return `null`.
* since: v1.63
- returns: <[FrameLocator]>

When working with iframes, you can create a frame locator that will search for elements in the main frame
and in all iframes on the page, so that you don't need to locate each iframe first.
When working with iframes, you can create a frame locator that will search for elements in this frame
and in all iframes inside it, so that you don't need to locate each iframe first.

Note that all elements matching the locator must belong to a single frame. For example, if the page contains
Note that all elements matching the locator must belong to a single frame. For example, if the frame contains
two iframes, each with a `Submit` button, piercing frames and locating a button will throw an error
because it matches elements from multiple frames.

**Usage**

Following snippet locates a button, either in the main frame or in one of the iframes:
Following snippet locates a button, either in the frame or in one of the iframes inside it:

```js
const locator = frame.pierceFrames().getByRole('button');
Expand Down
8 changes: 4 additions & 4 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8166,16 +8166,16 @@ export interface Frame {
parentFrame(): null|Frame;

/**
* When working with iframes, you can create a frame locator that will search for elements in the main frame and in
* all iframes on the page, so that you don't need to locate each iframe first.
* When working with iframes, you can create a frame locator that will search for elements in this frame and in all
* iframes inside it, so that you don't need to locate each iframe first.
*
* Note that all elements matching the locator must belong to a single frame. For example, if the page contains two
* Note that all elements matching the locator must belong to a single frame. For example, if the frame contains two
* iframes, each with a `Submit` button, piercing frames and locating a button will throw an error because it matches
* elements from multiple frames.
*
* **Usage**
*
* Following snippet locates a button, either in the main frame or in one of the iframes:
* Following snippet locates a button, either in the frame or in one of the iframes inside it:
*
* ```js
* const locator = frame.pierceFrames().getByRole('button');
Expand Down
26 changes: 13 additions & 13 deletions packages/playwright-core/src/server/frameSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,17 @@ export class FrameSelectors {
private async _resolveFramePiercingSelector(parsed: ParsedSelector, options: types.StrictOptions, scope: ElementHandle | undefined) {
const candidates = new Map<Frame, Set<number>>();
const infos = parsed.parts.map(part => this._parseSelector({ parts: [part] }, options));
for (const frame of this.frame._page.frameManager.frames())
await this._pierceFramesRecursivelyIfNotSeen(frame, infos, scope, 0, candidates);
await this._pierceFramesRecursivelyIfNotSeen(this.frame, infos, scope, 0, candidates);
const result: SelectorInFrame[] = [];
for (const [frame, matches] of candidates) {
for (const match of matches) {
const suffix = infos.slice(match);
for (const [frame, startIndexes] of candidates) {
for (const startIndex of startIndexes) {
const suffix = infos.slice(startIndex);
const partialInfo: SelectorInfo = {
parsed: { parts: suffix.map(info => info.parsed.parts[0]) },
world: suffix.some(info => info.world === 'main') ? 'main' : 'utility',
strict: !!options.strict,
};
result.push({ frame, info: partialInfo });
result.push({ frame, info: partialInfo, scope: frame === this.frame && startIndex === 0 ? scope : undefined });
}
}
return result;
Expand All @@ -206,7 +205,7 @@ export class FrameSelectors {
}
if (!set.has(startIndex)) {
set.add(startIndex);
await this._pierceFramesRecursively(frame, infos, undefined, startIndex, result);
await this._pierceFramesRecursively(frame, infos, scope, startIndex, result);
}
}

Expand All @@ -215,7 +214,8 @@ export class FrameSelectors {
const injected = await context.injectedScript();
const frameCandidatesHandle = await injected.evaluateHandle((injected, { infos, scope, startIndex }) => {
const frameElements = injected.querySelectorAll(injected.parseSelector('css=frame,iframe'), scope || document);
const result = frameElements.map(frameElement => ({ frameElement, matches: [] as number[] }));
// Any frame inside the search root may contain the whole selector suffix, so seed with startIndex.
const result = frameElements.map(frameElement => ({ frameElement, nextIndexes: [startIndex] }));

let roots = [scope || document];
for (let index = startIndex; index < infos.length; index++) {
Expand All @@ -227,9 +227,9 @@ export class FrameSelectors {
}
roots = [...next];
if (index + 1 < infos.length && !['nth', 'visible'].includes(infos[index + 1].parsed.parts[0].name)) {
for (const { frameElement, matches } of result) {
for (const { frameElement, nextIndexes } of result) {
if (roots.some(root => injected.utils.isInsideScope(root, frameElement)))
matches.push(index);
nextIndexes.push(index + 1);
}
}
}
Expand All @@ -242,9 +242,9 @@ export class FrameSelectors {
const frameElement = await frameCandidatesHandle.evaluateHandle((list, i) => list[i].frameElement, i) as ElementHandle<Element>;
const childFrame = await frame._page.delegate.getContentFrame(frameElement).catch(() => null);
if (childFrame) {
const matches = await frameCandidatesHandle.evaluate((list, i) => list[i].matches, i) as number[];
for (const match of matches)
await this._pierceFramesRecursivelyIfNotSeen(childFrame, infos, undefined, match + 1, result);
const nextIndexes = await frameCandidatesHandle.evaluate((list, i) => list[i].nextIndexes, i) as number[];
for (const nextIndex of nextIndexes)
await this._pierceFramesRecursivelyIfNotSeen(childFrame, infos, undefined, nextIndex, result);
}
} catch {
// Ignore errors for this frame candidate.
Expand Down
8 changes: 4 additions & 4 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8166,16 +8166,16 @@ export interface Frame {
parentFrame(): null|Frame;

/**
* When working with iframes, you can create a frame locator that will search for elements in the main frame and in
* all iframes on the page, so that you don't need to locate each iframe first.
* When working with iframes, you can create a frame locator that will search for elements in this frame and in all
* iframes inside it, so that you don't need to locate each iframe first.
*
* Note that all elements matching the locator must belong to a single frame. For example, if the page contains two
* Note that all elements matching the locator must belong to a single frame. For example, if the frame contains two
* iframes, each with a `Submit` button, piercing frames and locating a button will throw an error because it matches
* elements from multiple frames.
*
* **Usage**
*
* Following snippet locates a button, either in the main frame or in one of the iframes:
* Following snippet locates a button, either in the frame or in one of the iframes inside it:
*
* ```js
* const locator = frame.pierceFrames().getByRole('button');
Expand Down
62 changes: 62 additions & 0 deletions tests/page/locator-pierce-frames.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,68 @@ it('should support first/last/nth as the last operation', async ({ page, server
await expect(page.pierceFrames().locator('span').nth(1)).toHaveText('two');
});

it('should pierce a frame inside the scope', async ({ page, server }) => {
await routePage(page, 'empty.html', `<section><iframe src="a.html"></iframe></section><button>outside</button>`);
await routePage(page, 'a.html', `<button>inside</button>`);
await page.goto(server.EMPTY_PAGE);
await waitForAllFrames(page, 2, 'button');
const scope = (await page.$('section'))!;
const buttons = await scope.$$('internal:control=pierce-frames >> button');
expect(buttons.length).toBe(1);
expect(await buttons[0].textContent()).toBe('inside');
});

it('should pierce a nested frame inside the scope', async ({ page, server }) => {
await routePage(page, 'empty.html', `<section><iframe src="a.html"></iframe></section><iframe src="b.html"></iframe>`);
await routePage(page, 'a.html', `<iframe src="c.html"></iframe>`);
await routePage(page, 'b.html', `<button>outside</button>`);
await routePage(page, 'c.html', `<button>deep</button>`);
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.frames().length).toBe(4);
for (const frame of page.frames()) {
if (frame.url().includes('b.html') || frame.url().includes('c.html'))
await frame.waitForSelector('button', { state: 'attached' });
}
const scope = (await page.$('section'))!;
const buttons = await scope.$$('internal:control=pierce-frames >> button');
expect(buttons.length).toBe(1);
expect(await buttons[0].textContent()).toBe('deep');
});

it('should respect the scope without a frame inside the scope', async ({ page, server }) => {
await routePage(page, 'empty.html', `<section><button>target</button></section><iframe src="a.html"></iframe>`);
await routePage(page, 'a.html', `<button>in-frame</button>`);
await page.goto(server.EMPTY_PAGE);
await waitForAllFrames(page, 2, 'button');
const scope = (await page.$('section'))!;
const buttons = await scope.$$('internal:control=pierce-frames >> button');
expect(buttons.length).toBe(1);
expect(await buttons[0].textContent()).toBe('target');
});

it('should pierce nested frames below a matching prefix', async ({ page, server }) => {
await routePage(page, 'empty.html', `<section><iframe src="a.html"></iframe></section>`);
await routePage(page, 'a.html', `<iframe src="b.html"></iframe>`);
await routePage(page, 'b.html', `<button>deep</button>`);
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.frames().length).toBe(3);
const deepFrame = page.frames().find(f => f.url().includes('b.html'))!;
await deepFrame.waitForSelector('button', { state: 'attached' });
await expect(page.pierceFrames().locator('section').locator('button')).toHaveText('deep');
});

it('should pierce only frames inside the starting frame', async ({ page, server }) => {
await routePage(page, 'empty.html', `<iframe src="a.html"></iframe><button>main</button>`);
await routePage(page, 'a.html', `<iframe src="b.html"></iframe>`);
await routePage(page, 'b.html', `<button>deep</button>`);
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.frames().length).toBe(3);
const deepFrame = page.frames().find(f => f.url().includes('b.html'))!;
await deepFrame.waitForSelector('button', { state: 'attached' });
const middleFrame = page.frames().find(f => f.url().includes('a.html'))!;
await expect(middleFrame.pierceFrames().locator('button')).toHaveText('deep');
});

it('should not allow nth in the middle', async ({ page }) => {
const error = await page.pierceFrames().locator('div').first().locator('span').count().catch(e => e);
expect(error.message).toContain(`nth can only be the last locator when piercing frames, while querying "pierceFrames().locator('div').first().locator('span')"`);
Expand Down
Loading