diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
index 5cd51a82a0336..b624c4e44ae25 100644
--- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
@@ -272,11 +272,11 @@ export class FrameDispatcher extends Dispatcher {
- return await progress.race(this._frame._page.highlightController.addHighlight(params.selector, { style: params.style }));
+ return await progress.race(this._frame._page.highlightController.addHighlight(params.selector, { style: params.style, frame: this._frame }));
}
async hideHighlight(params: channels.FrameHideHighlightParams, progress: Progress): Promise {
- return await progress.race(this._frame._page.highlightController.removeHighlight(params.selector));
+ return await progress.race(this._frame._page.highlightController.removeHighlight(params.selector, this._frame));
}
async expect(params: channels.FrameExpectParams, progress: Progress): Promise {
diff --git a/packages/playwright-core/src/server/highlightController.ts b/packages/playwright-core/src/server/highlightController.ts
index 69b829bccdd0f..37358aaf76d24 100644
--- a/packages/playwright-core/src/server/highlightController.ts
+++ b/packages/playwright-core/src/server/highlightController.ts
@@ -14,18 +14,22 @@
* limitations under the License.
*/
+import { Page } from './page';
+
import type { FrameExecutionContext } from './dom';
-import type { Page } from './page';
+import type { Frame } from './frames';
import type * as types from './types';
import type { ParsedSelector } from '@isomorphic/selectorParser';
export type HighlightOptions = {
style?: string;
anyFrame?: boolean; // Highlight in all the frames the selector could resolve to, instead of a single one.
+ frame?: Frame; // Resolve the selector relative to this frame, defaults to the main frame.
};
type HighlightEntry = HighlightOptions & {
selector: string;
+ frame: Frame;
};
// Custom selector engines run in the main world, so highlights may live in either world.
@@ -39,20 +43,31 @@ export class HighlightController {
constructor(page: Page) {
this._page = page;
+ page.on(Page.Events.FrameDetached, frame => {
+ for (const [key, entry] of this._entries) {
+ if (entry.frame === frame)
+ this._entries.delete(key);
+ }
+ });
}
async addHighlight(selector: string, options: HighlightOptions = {}) {
// Validate the selector upfront, so that the caller gets a synchronous error.
this._page.browserContext.selectors().parseSelector(selector, false);
- this._entries.set(selector, { selector, ...options });
+ const frame = options.frame ?? this._page.mainFrame();
+ this._entries.set(this._key(frame, selector), { selector, ...options, frame });
await this._resolveNow();
}
- async removeHighlight(selector: string) {
- this._entries.delete(selector);
+ async removeHighlight(selector: string, frame?: Frame) {
+ this._entries.delete(this._key(frame ?? this._page.mainFrame(), selector));
await this._resolveNow();
}
+ private _key(frame: Frame, selector: string) {
+ return frame.guid + ':' + selector;
+ }
+
dispose() {
if (this._resolutionTimer) {
clearTimeout(this._resolutionTimer);
@@ -85,7 +100,7 @@ export class HighlightController {
const perContext = new Map();
for (const entry of this._entries.values()) {
- const results = await this._page.mainFrame().selectors.resolveFramesForSelector(entry.selector, { strict: false, anyFrame: entry.anyFrame }).catch(() => []);
+ const results = await entry.frame.selectors.resolveFramesForSelector(entry.selector, { strict: false, anyFrame: entry.anyFrame }).catch(() => []);
for (const { frame, info } of results) {
const context = frame.existingContext(info.world);
if (!context)
diff --git a/tests/library/locator-highlight.spec.ts b/tests/library/locator-highlight.spec.ts
index 7b18e1bf20dd3..35f3c4fc0f406 100644
--- a/tests/library/locator-highlight.spec.ts
+++ b/tests/library/locator-highlight.spec.ts
@@ -151,3 +151,24 @@ test('highlight should work with a custom selector engine that runs in the main
await context.close();
});
+
+test('highlight should resolve relative to the frame of the locator', async ({ browser }) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ await page.setContent('');
+ const frame = page.frame('frame')!;
+
+ await page.locator('button').highlight();
+ await frame.locator('button').highlight();
+ await expect(page.locator('x-pw-highlight')).toHaveCount(1);
+ await expect(frame.locator('x-pw-highlight')).toHaveCount(1);
+
+ await page.locator('button').hideHighlight();
+ await expect(page.locator('x-pw-highlight')).toHaveCount(0);
+ await expect(frame.locator('x-pw-highlight')).toHaveCount(1);
+
+ await frame.locator('button').hideHighlight();
+ await expect(frame.locator('x-pw-highlight')).toHaveCount(0);
+
+ await context.close();
+});