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>;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue