fix(trace): make locator picker work for iframes (#26883)

Fixes https://github.com/microsoft/playwright/issues/26878
This commit is contained in:
Pavel Feldman 2023-09-06 09:44:47 -07:00 committed by GitHub
parent 500821d1bd
commit b4012df160
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 32 deletions

View file

@ -25,6 +25,7 @@ interface RecorderDelegate {
performAction?(action: actions.Action): Promise<void>;
recordAction?(action: actions.Action): Promise<void>;
setSelector?(selector: string): Promise<void>;
highlightUpdated?(): void;
}
export class Recorder {
@ -41,16 +42,13 @@ export class Recorder {
private _highlight: Highlight;
private _testIdAttributeName: string = 'data-testid';
readonly document: Document;
private _delegate: RecorderDelegate;
private _delegate: RecorderDelegate = {};
constructor(injectedScript: InjectedScript, delegate: RecorderDelegate) {
constructor(injectedScript: InjectedScript) {
this.document = injectedScript.document;
this._injectedScript = injectedScript;
this._delegate = delegate;
this._highlight = new Highlight(injectedScript);
this.refreshListenersIfNeeded();
if (injectedScript.isUnderTest)
console.error('Recorder script ready for test'); // eslint-disable-line no-console
}
@ -82,13 +80,20 @@ export class Recorder {
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;
this._testIdAttributeName = testIdAttributeName;
this._highlight.setLanguage(language);
if (mode !== this._mode) {
this._mode = mode;
this._clearHighlight();
this.clearHighlight();
}
if (actionPoint && this._actionPoint && actionPoint.x === this._actionPoint.x && actionPoint.y === this._actionPoint.y) {
// All good.
@ -113,7 +118,7 @@ export class Recorder {
}
}
private _clearHighlight() {
clearHighlight() {
this._hoveredModel = null;
this._activeModel = null;
this._updateHighlight();
@ -267,6 +272,8 @@ export class Recorder {
const elements = this._hoveredModel ? this._hoveredModel.elements : [];
const selector = this._hoveredModel ? this._hoveredModel.selector : '';
this._highlight.updateHighlight(elements, selector, this._mode === 'recording');
if (this._hoveredModel)
this._delegate.highlightUpdated?.();
}
private _onInput(event: Event) {
@ -398,7 +405,7 @@ export class Recorder {
}
private async _performAction(action: actions.Action) {
this._clearHighlight();
this.clearHighlight();
this._performingAction = true;
await this._delegate.performAction?.(action).catch(() => {});
this._performingAction = false;
@ -512,7 +519,7 @@ export class PollingRecorder implements RecorderDelegate {
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
constructor(injectedScript: InjectedScript) {
this._recorder = new Recorder(injectedScript, this);
this._recorder = new Recorder(injectedScript);
this._embedder = injectedScript.window as any;
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.refreshListenersIfNeeded());
@ -533,7 +540,7 @@ export class PollingRecorder implements RecorderDelegate {
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
return;
}
this._recorder.setUIState(state);
this._recorder.setUIState(state, this);
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
}

View file

@ -227,37 +227,54 @@ export const InspectModeController: React.FunctionComponent<{
iteration: number,
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator, iteration }) => {
React.useEffect(() => {
const win = iframe?.contentWindow as any;
let recorder: Recorder | undefined;
const recorders: { recorder: Recorder, frameSelector: string }[] = [];
try {
if (!win)
return;
recorder = win._recorder;
if (!recorder && !isInspecting && !highlightedLocator)
return;
createRecorders(recorders, sdkLanguage, testIdAttributeName, '', iframe?.contentWindow);
} catch {
// Potential cross-origin exception when accessing win._recorder.
return;
// Potential cross-origin exceptions.
}
if (!recorder) {
const injectedScript = new InjectedScript(win, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
recorder = new Recorder(injectedScript, {
for (const { recorder, frameSelector } of recorders) {
const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, highlightedLocator, testIdAttributeName);
recorder.setUIState({
mode: isInspecting ? 'inspecting' : 'none',
actionSelector,
language: sdkLanguage,
testIdAttributeName,
}, {
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]);
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 kBlankSnapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';

View file

@ -996,3 +996,41 @@ test('should ignore 304 responses', async ({ page, server, runAndTrace }) => {
const frame = await traceViewer.snapshotFrame('locator.click');
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 });
});