diff --git a/packages/playwright-core/src/server/injected/recorder.ts b/packages/playwright-core/src/server/injected/recorder.ts index 5b7b6188da..3ab2f9bd6b 100644 --- a/packages/playwright-core/src/server/injected/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder.ts @@ -53,7 +53,7 @@ export class Recorder { console.error('Recorder script ready for test'); // eslint-disable-line no-console } - refreshListenersIfNeeded() { + installListeners() { // Ensure we are attached to the current document, and we are on top (last element); if (this._highlight.isInstalled()) return; @@ -74,19 +74,24 @@ export class Recorder { return; this._hoveredModel = null; this._highlight.hideActionPoint(); - this._updateHighlight(); + this._updateHighlight(false); }, true), ]; this._highlight.install(); } + uninstallListeners() { + removeEventListeners(this._listeners); + this._highlight.uninstall(); + } + setUIState(state: UIState, delegate: RecorderDelegate) { this._delegate = delegate; if (state.mode !== 'none' || state.actionSelector) - this.refreshListenersIfNeeded(); + this.installListeners(); else - removeEventListeners(this._listeners); + this.uninstallListeners(); const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state; this._testIdAttributeName = testIdAttributeName; @@ -113,7 +118,7 @@ export class Recorder { if (actionSelector !== this._actionSelector) { this._hoveredModel = actionSelector ? querySelector(this._injectedScript, actionSelector, this.document) : null; - this._updateHighlight(); + this._updateHighlight(false); this._actionSelector = actionSelector; } } @@ -121,7 +126,7 @@ export class Recorder { clearHighlight() { this._hoveredModel = null; this._activeModel = null; - this._updateHighlight(); + this._updateHighlight(false); } private _actionInProgress(event: Event): boolean { @@ -257,7 +262,7 @@ export class Recorder { if (!this._hoveredElement || !this._hoveredElement.isConnected) { this._hoveredModel = null; this._hoveredElement = null; - this._updateHighlight(); + this._updateHighlight(true); return; } const hoveredElement = this._hoveredElement; @@ -265,14 +270,14 @@ export class Recorder { if ((this._hoveredModel && this._hoveredModel.selector === selector)) return; this._hoveredModel = selector ? { selector, elements } : null; - this._updateHighlight(); + this._updateHighlight(true); } - private _updateHighlight() { + private _updateHighlight(userGesture: boolean) { 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) + if (userGesture) this._delegate.highlightUpdated?.(); } @@ -522,7 +527,7 @@ export class PollingRecorder implements RecorderDelegate { this._recorder = new Recorder(injectedScript); this._embedder = injectedScript.window as any; - injectedScript.onGlobalListenersRemoved.add(() => this._recorder.refreshListenersIfNeeded()); + injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners()); const refreshOverlay = () => { this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index cbebb86c24..39ed758421 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -228,8 +228,9 @@ export const InspectModeController: React.FunctionComponent<{ }> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator, iteration }) => { React.useEffect(() => { const recorders: { recorder: Recorder, frameSelector: string }[] = []; + const isUnderTest = new URLSearchParams(window.location.search).get('isUnderTest') === 'true'; try { - createRecorders(recorders, sdkLanguage, testIdAttributeName, '', iframe?.contentWindow); + createRecorders(recorders, sdkLanguage, testIdAttributeName, isUnderTest, '', iframe?.contentWindow); } catch { // Potential cross-origin exceptions. } @@ -238,7 +239,7 @@ export const InspectModeController: React.FunctionComponent<{ const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, highlightedLocator, testIdAttributeName); recorder.setUIState({ mode: isInspecting ? 'inspecting' : 'none', - actionSelector, + actionSelector: actionSelector.startsWith(frameSelector) ? actionSelector.substring(frameSelector.length).trim() : undefined, language: sdkLanguage, testIdAttributeName, }, { @@ -257,12 +258,12 @@ export const InspectModeController: React.FunctionComponent<{ return <>; }; -function createRecorders(recorders: { recorder: Recorder, frameSelector: string }[], sdkLanguage: Language, testIdAttributeName: string, parentFrameSelector: string, frameWindow: Window | null | undefined) { +function createRecorders(recorders: { recorder: Recorder, frameSelector: string }[], sdkLanguage: Language, testIdAttributeName: string, isUnderTest: boolean, 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 injectedScript = new InjectedScript(frameWindow as any, isUnderTest, sdkLanguage, testIdAttributeName, 1, 'chromium', []); const recorder = new Recorder(injectedScript); win._injectedScript = injectedScript; win._recorder = { recorder, frameSelector: parentFrameSelector }; @@ -272,7 +273,7 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string 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); + createRecorders(recorders, sdkLanguage, testIdAttributeName, isUnderTest, parentFrameSelector + frameSelector, childFrame); } } diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 3f06591f24..75a756c9ff 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1034,3 +1034,58 @@ test('should pick locator in iframe', async ({ page, runAndTrace, server }) => { 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 }); }); + +test('should highlight locator in iframe while typing', async ({ page, runAndTrace, server, platform }) => { + /* + 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(`