fix(trace): make locator picker work for iframes (#26883)
Fixes https://github.com/microsoft/playwright/issues/26878
This commit is contained in:
parent
500821d1bd
commit
b4012df160
|
|
@ -25,6 +25,7 @@ interface RecorderDelegate {
|
||||||
performAction?(action: actions.Action): Promise<void>;
|
performAction?(action: actions.Action): Promise<void>;
|
||||||
recordAction?(action: actions.Action): Promise<void>;
|
recordAction?(action: actions.Action): Promise<void>;
|
||||||
setSelector?(selector: string): Promise<void>;
|
setSelector?(selector: string): Promise<void>;
|
||||||
|
highlightUpdated?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Recorder {
|
export class Recorder {
|
||||||
|
|
@ -41,16 +42,13 @@ export class Recorder {
|
||||||
private _highlight: Highlight;
|
private _highlight: Highlight;
|
||||||
private _testIdAttributeName: string = 'data-testid';
|
private _testIdAttributeName: string = 'data-testid';
|
||||||
readonly document: Document;
|
readonly document: Document;
|
||||||
private _delegate: RecorderDelegate;
|
private _delegate: RecorderDelegate = {};
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript, delegate: RecorderDelegate) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this.document = injectedScript.document;
|
this.document = injectedScript.document;
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
this._delegate = delegate;
|
|
||||||
this._highlight = new Highlight(injectedScript);
|
this._highlight = new Highlight(injectedScript);
|
||||||
|
|
||||||
this.refreshListenersIfNeeded();
|
|
||||||
|
|
||||||
if (injectedScript.isUnderTest)
|
if (injectedScript.isUnderTest)
|
||||||
console.error('Recorder script ready for test'); // eslint-disable-line no-console
|
console.error('Recorder script ready for test'); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
@ -82,13 +80,20 @@ export class Recorder {
|
||||||
this._highlight.install();
|
this._highlight.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
setUIState(state: UIState) {
|
setUIState(state: UIState, delegate: RecorderDelegate) {
|
||||||
|
this._delegate = delegate;
|
||||||
|
|
||||||
|
if (state.mode !== 'none' || state.actionSelector)
|
||||||
|
this.refreshListenersIfNeeded();
|
||||||
|
else
|
||||||
|
removeEventListeners(this._listeners);
|
||||||
|
|
||||||
const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state;
|
const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state;
|
||||||
this._testIdAttributeName = testIdAttributeName;
|
this._testIdAttributeName = testIdAttributeName;
|
||||||
this._highlight.setLanguage(language);
|
this._highlight.setLanguage(language);
|
||||||
if (mode !== this._mode) {
|
if (mode !== this._mode) {
|
||||||
this._mode = mode;
|
this._mode = mode;
|
||||||
this._clearHighlight();
|
this.clearHighlight();
|
||||||
}
|
}
|
||||||
if (actionPoint && this._actionPoint && actionPoint.x === this._actionPoint.x && actionPoint.y === this._actionPoint.y) {
|
if (actionPoint && this._actionPoint && actionPoint.x === this._actionPoint.x && actionPoint.y === this._actionPoint.y) {
|
||||||
// All good.
|
// All good.
|
||||||
|
|
@ -113,7 +118,7 @@ export class Recorder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearHighlight() {
|
clearHighlight() {
|
||||||
this._hoveredModel = null;
|
this._hoveredModel = null;
|
||||||
this._activeModel = null;
|
this._activeModel = null;
|
||||||
this._updateHighlight();
|
this._updateHighlight();
|
||||||
|
|
@ -267,6 +272,8 @@ export class Recorder {
|
||||||
const elements = this._hoveredModel ? this._hoveredModel.elements : [];
|
const elements = this._hoveredModel ? this._hoveredModel.elements : [];
|
||||||
const selector = this._hoveredModel ? this._hoveredModel.selector : '';
|
const selector = this._hoveredModel ? this._hoveredModel.selector : '';
|
||||||
this._highlight.updateHighlight(elements, selector, this._mode === 'recording');
|
this._highlight.updateHighlight(elements, selector, this._mode === 'recording');
|
||||||
|
if (this._hoveredModel)
|
||||||
|
this._delegate.highlightUpdated?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onInput(event: Event) {
|
private _onInput(event: Event) {
|
||||||
|
|
@ -398,7 +405,7 @@ export class Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _performAction(action: actions.Action) {
|
private async _performAction(action: actions.Action) {
|
||||||
this._clearHighlight();
|
this.clearHighlight();
|
||||||
this._performingAction = true;
|
this._performingAction = true;
|
||||||
await this._delegate.performAction?.(action).catch(() => {});
|
await this._delegate.performAction?.(action).catch(() => {});
|
||||||
this._performingAction = false;
|
this._performingAction = false;
|
||||||
|
|
@ -512,7 +519,7 @@ export class PollingRecorder implements RecorderDelegate {
|
||||||
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._recorder = new Recorder(injectedScript, this);
|
this._recorder = new Recorder(injectedScript);
|
||||||
this._embedder = injectedScript.window as any;
|
this._embedder = injectedScript.window as any;
|
||||||
|
|
||||||
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.refreshListenersIfNeeded());
|
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.refreshListenersIfNeeded());
|
||||||
|
|
@ -533,7 +540,7 @@ export class PollingRecorder implements RecorderDelegate {
|
||||||
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._recorder.setUIState(state);
|
this._recorder.setUIState(state, this);
|
||||||
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,37 +227,54 @@ export const InspectModeController: React.FunctionComponent<{
|
||||||
iteration: number,
|
iteration: number,
|
||||||
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator, iteration }) => {
|
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator, iteration }) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const win = iframe?.contentWindow as any;
|
const recorders: { recorder: Recorder, frameSelector: string }[] = [];
|
||||||
let recorder: Recorder | undefined;
|
|
||||||
try {
|
try {
|
||||||
if (!win)
|
createRecorders(recorders, sdkLanguage, testIdAttributeName, '', iframe?.contentWindow);
|
||||||
return;
|
|
||||||
recorder = win._recorder;
|
|
||||||
if (!recorder && !isInspecting && !highlightedLocator)
|
|
||||||
return;
|
|
||||||
} catch {
|
} catch {
|
||||||
// Potential cross-origin exception when accessing win._recorder.
|
// Potential cross-origin exceptions.
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!recorder) {
|
|
||||||
const injectedScript = new InjectedScript(win, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
for (const { recorder, frameSelector } of recorders) {
|
||||||
recorder = new Recorder(injectedScript, {
|
const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, highlightedLocator, testIdAttributeName);
|
||||||
|
recorder.setUIState({
|
||||||
|
mode: isInspecting ? 'inspecting' : 'none',
|
||||||
|
actionSelector,
|
||||||
|
language: sdkLanguage,
|
||||||
|
testIdAttributeName,
|
||||||
|
}, {
|
||||||
async setSelector(selector: string) {
|
async setSelector(selector: string) {
|
||||||
setHighlightedLocator(asLocator(sdkLanguage, selector, false /* isFrameLocator */, true /* playSafe */));
|
setHighlightedLocator(asLocator(sdkLanguage, frameSelector + selector, false /* isFrameLocator */, true /* playSafe */));
|
||||||
|
},
|
||||||
|
highlightUpdated() {
|
||||||
|
for (const r of recorders) {
|
||||||
|
if (r.recorder !== recorder)
|
||||||
|
r.recorder.clearHighlight();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
win._recorder = recorder;
|
|
||||||
}
|
}
|
||||||
const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, highlightedLocator, testIdAttributeName);
|
|
||||||
recorder.setUIState({
|
|
||||||
mode: isInspecting ? 'inspecting' : 'none',
|
|
||||||
actionSelector,
|
|
||||||
language: sdkLanguage,
|
|
||||||
testIdAttributeName,
|
|
||||||
});
|
|
||||||
}, [iframe, isInspecting, highlightedLocator, setHighlightedLocator, sdkLanguage, testIdAttributeName, iteration]);
|
}, [iframe, isInspecting, highlightedLocator, setHighlightedLocator, sdkLanguage, testIdAttributeName, iteration]);
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createRecorders(recorders: { recorder: Recorder, frameSelector: string }[], sdkLanguage: Language, testIdAttributeName: string, parentFrameSelector: string, frameWindow: Window | null | undefined) {
|
||||||
|
if (!frameWindow)
|
||||||
|
return;
|
||||||
|
const win = frameWindow as any;
|
||||||
|
if (!win._recorder) {
|
||||||
|
const injectedScript = new InjectedScript(frameWindow as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
||||||
|
const recorder = new Recorder(injectedScript);
|
||||||
|
win._injectedScript = injectedScript;
|
||||||
|
win._recorder = { recorder, frameSelector: parentFrameSelector };
|
||||||
|
}
|
||||||
|
recorders.push(win._recorder);
|
||||||
|
|
||||||
|
for (let i = 0; i < frameWindow.frames.length; ++i) {
|
||||||
|
const childFrame = frameWindow.frames[i];
|
||||||
|
const frameSelector = childFrame.frameElement ? win._injectedScript.generateSelector(childFrame.frameElement, { omitInternalEngines: true, testIdAttributeName }) + ' >> internal:control=enter-frame >> ' : '';
|
||||||
|
createRecorders(recorders, sdkLanguage, testIdAttributeName, parentFrameSelector + frameSelector, childFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const kDefaultViewport = { width: 1280, height: 720 };
|
const kDefaultViewport = { width: 1280, height: 720 };
|
||||||
const kBlankSnapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';
|
const kBlankSnapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';
|
||||||
|
|
|
||||||
|
|
@ -996,3 +996,41 @@ test('should ignore 304 responses', async ({ page, server, runAndTrace }) => {
|
||||||
const frame = await traceViewer.snapshotFrame('locator.click');
|
const frame = await traceViewer.snapshotFrame('locator.click');
|
||||||
await expect(frame.locator('body')).toHaveCSS('background-color', 'rgb(123, 123, 123)');
|
await expect(frame.locator('body')).toHaveCSS('background-color', 'rgb(123, 123, 123)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should pick locator in iframe', async ({ page, runAndTrace, server }) => {
|
||||||
|
/*
|
||||||
|
iframe[id=frame1]
|
||||||
|
div Hello1
|
||||||
|
iframe
|
||||||
|
div Hello2
|
||||||
|
iframe[name=one]
|
||||||
|
div HelloNameOne
|
||||||
|
iframe[name=two]
|
||||||
|
dev HelloNameTwo
|
||||||
|
*/
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.setContent(`<iframe id=frame1 srcdoc="<div>Hello1</div><iframe srcdoc='<div>Hello2</div><iframe name=one></iframe><iframe name=two></iframe><iframe></iframe>'>">`);
|
||||||
|
const frameOne = page.frame({ name: 'one' });
|
||||||
|
await frameOne.setContent(`<div>HelloNameOne</div>`);
|
||||||
|
const frameTwo = page.frame({ name: 'two' });
|
||||||
|
await frameTwo.setContent(`<div>HelloNameTwo</div>`);
|
||||||
|
await page.evaluate('2+2');
|
||||||
|
});
|
||||||
|
await traceViewer.page.getByTitle('Pick locator').click();
|
||||||
|
const cmWrapper = traceViewer.page.locator('.cm-wrapper');
|
||||||
|
|
||||||
|
const snapshot = await traceViewer.snapshotFrame('page.evaluate');
|
||||||
|
|
||||||
|
await snapshot.frameLocator('#frame1').getByText('Hello1').click();
|
||||||
|
await expect.soft(cmWrapper).toContainText(`frameLocator('#frame1').getByText('Hello1')`);
|
||||||
|
|
||||||
|
await snapshot.frameLocator('#frame1').frameLocator('iframe').getByText('Hello2').click();
|
||||||
|
await expect.soft(cmWrapper).toContainText(`frameLocator('#frame1').frameLocator('iframe').getByText('Hello2')`, { timeout: 0 });
|
||||||
|
|
||||||
|
await snapshot.frameLocator('#frame1').frameLocator('iframe').frameLocator('[name=one]').getByText('HelloNameOne').click();
|
||||||
|
await expect.soft(cmWrapper).toContainText(`frameLocator('#frame1').frameLocator('iframe').frameLocator('iframe[name="one"]').getByText('HelloNameOne')`, { timeout: 0 });
|
||||||
|
|
||||||
|
await snapshot.frameLocator('#frame1').frameLocator('iframe').frameLocator('[name=two]').getByText('HelloNameTwo').click();
|
||||||
|
await expect.soft(cmWrapper).toContainText(`frameLocator('#frame1').frameLocator('iframe').frameLocator('iframe[name="two"]').getByText('HelloNameTwo')`, { timeout: 0 });
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue