fix(debug controller): highlight selectors in iframe
This commit is contained in:
parent
67471cb3c5
commit
95a7a757f0
|
|
@ -19,7 +19,7 @@ import { XPathEngine } from './xpathSelectorEngine';
|
||||||
import { ReactEngine } from './reactSelectorEngine';
|
import { ReactEngine } from './reactSelectorEngine';
|
||||||
import { VueEngine } from './vueSelectorEngine';
|
import { VueEngine } from './vueSelectorEngine';
|
||||||
import { createRoleEngine } from './roleSelectorEngine';
|
import { createRoleEngine } from './roleSelectorEngine';
|
||||||
import { parseAttributeSelector } from '../../utils/isomorphic/selectorParser';
|
import { parseAttributeSelector, splitSelectorByFrame } from '../../utils/isomorphic/selectorParser';
|
||||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../../utils/isomorphic/selectorParser';
|
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../../utils/isomorphic/selectorParser';
|
||||||
import { visitAllSelectorParts, parseSelector, stringifySelector } from '../../utils/isomorphic/selectorParser';
|
import { visitAllSelectorParts, parseSelector, stringifySelector } from '../../utils/isomorphic/selectorParser';
|
||||||
import { type TextMatcher, elementMatchesText, elementText, type ElementText, getElementLabels } from './selectorUtils';
|
import { type TextMatcher, elementMatchesText, elementText, type ElementText, getElementLabels } from './selectorUtils';
|
||||||
|
|
@ -166,12 +166,23 @@ export class InjectedScript {
|
||||||
return this._testIdAttributeNameForStrictErrorAndConsoleCodegen;
|
return this._testIdAttributeNameForStrictErrorAndConsoleCodegen;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSelector(selector: string): ParsedSelector {
|
private _checkForUnknownEngine(parsed: ParsedSelector, selector: string) {
|
||||||
const result = parseSelector(selector);
|
visitAllSelectorParts(parsed, part => {
|
||||||
visitAllSelectorParts(result, part => {
|
|
||||||
if (!this._engines.has(part.name))
|
if (!this._engines.has(part.name))
|
||||||
throw this.createStacklessError(`Unknown engine "${part.name}" while parsing selector ${selector}`);
|
throw this.createStacklessError(`Unknown engine "${part.name}" while parsing selector ${selector}`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSelector(selector: string): ParsedSelector {
|
||||||
|
const result = parseSelector(selector);
|
||||||
|
this._checkForUnknownEngine(result, selector);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
splitSelectorByFrame(selector: string): ParsedSelector[] {
|
||||||
|
const result = splitSelectorByFrame(selector);
|
||||||
|
for (const part of result)
|
||||||
|
this._checkForUnknownEngine(part, selector);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import type { ElementInfo, Mode, OverlayState, UIState } from '@recorder/recorde
|
||||||
import type { ElementText } from '../selectorUtils';
|
import type { ElementText } from '../selectorUtils';
|
||||||
import type { Highlight, HighlightOptions } from '../highlight';
|
import type { Highlight, HighlightOptions } from '../highlight';
|
||||||
import clipPaths from './clipPaths';
|
import clipPaths from './clipPaths';
|
||||||
|
import { parseSelector, splitSelectorByFrame } from '@isomorphic/selectorParser';
|
||||||
|
|
||||||
export interface RecorderDelegate {
|
export interface RecorderDelegate {
|
||||||
performAction?(action: actions.PerformOnRecordAction): Promise<void>;
|
performAction?(action: actions.PerformOnRecordAction): Promise<void>;
|
||||||
|
|
@ -1471,18 +1472,29 @@ function removeEventListeners(listeners: (() => void)[]) {
|
||||||
listeners.splice(0, listeners.length);
|
listeners.splice(0, listeners.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function frameDepth() {
|
||||||
|
let depth = 0;
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
let w: Window = window;
|
||||||
|
while (w.parent !== w) {
|
||||||
|
w = w.parent;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
|
function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
|
||||||
|
const empty = { selector, elements: [] };
|
||||||
try {
|
try {
|
||||||
const parsedSelector = injectedScript.parseSelector(selector);
|
// selector starts from the root frame, we need to figure out if this frame is the right one
|
||||||
return {
|
// as a cheap heuristic, we check if it targets the right depth
|
||||||
selector,
|
const parts = injectedScript.splitSelectorByFrame(selector);
|
||||||
elements: injectedScript.querySelectorAll(parsedSelector, ownerDocument)
|
const selectorTargetsFrameDepth = parts.length - 1 === frameDepth();
|
||||||
};
|
if (selectorTargetsFrameDepth)
|
||||||
|
return { selector, elements: injectedScript.querySelectorAll(parts[parts.length - 1], ownerDocument) };
|
||||||
|
return empty;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return empty;
|
||||||
selector,
|
|
||||||
elements: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,8 +246,10 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshOverlay() {
|
private _refreshOverlay() {
|
||||||
for (const page of this._context.pages())
|
for (const page of this._context.pages()) {
|
||||||
page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
|
for (const frame of page.frames())
|
||||||
|
frame.evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
|
|
|
||||||
|
|
@ -253,3 +253,22 @@ test('should reset routes before reuse', async ({ server, connectedBrowserFactor
|
||||||
await expect(page2).toHaveTitle('console.log test');
|
await expect(page2).toHaveTitle('console.log test');
|
||||||
await browser2.close();
|
await browser2.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should highlight inside iframe', async ({ backend, connectedBrowser }, testInfo) => {
|
||||||
|
testInfo.annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33146' });
|
||||||
|
|
||||||
|
const context = await connectedBrowser._newContextForReuse();
|
||||||
|
const page = await context.newPage();
|
||||||
|
await backend.navigate({ url: `data:text/html,<iframe srcdoc="<div>bar</div>"/>` });
|
||||||
|
|
||||||
|
|
||||||
|
await page.frameLocator('iframe').getByText('bar').highlight();
|
||||||
|
|
||||||
|
const highlight = page.frameLocator('iframe').locator('x-pw-highlight');
|
||||||
|
await expect(highlight).not.toHaveCount(0);
|
||||||
|
await backend.hideHighlight();
|
||||||
|
await expect(highlight).toHaveCount(0);
|
||||||
|
|
||||||
|
await backend.highlight({ selector: `frameLocator('iframe').getByText('bar')` });
|
||||||
|
await expect(highlight).not.toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue