diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 1828af3bd0cd8..b905ba8548a05 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -1018,7 +1018,7 @@ export class Recorder { private _listeners: (() => void)[] = []; private _currentTool: RecorderTool; private _tools: Record; - private _actionSelectorModel: HighlightModel | null = null; + private _lastHighlightedSelector: string | undefined = undefined; private _lastHighlightedAriaTemplateJSON: string = 'undefined'; readonly highlight: Highlight; readonly overlay: Overlay | undefined; @@ -1129,12 +1129,12 @@ export class Recorder { this._switchCurrentTool(); this.overlay?.setUIState(state); - // Race or scroll. - if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length && !this._lastHighlightedAriaTemplateJSON) - this._actionSelectorModel = null; - - if (state.actionSelector && state.actionSelector !== this._actionSelectorModel?.selector) - this._actionSelectorModel = querySelector(this.injectedScript, state.actionSelector, this.document); + let highlight: HighlightModel | 'clear' | 'noop' = 'noop'; + if (state.actionSelector !== this._lastHighlightedSelector) { + this._lastHighlightedSelector = state.actionSelector; + const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null; + highlight = model?.elements.length ? model : 'clear'; + } const ariaTemplateJSON = JSON.stringify(state.ariaTemplate); if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) { @@ -1142,16 +1142,15 @@ export class Recorder { const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined; const elements = template ? this.injectedScript.getAllByAria(this.document, template) : []; if (elements.length) - this._actionSelectorModel = { elements }; + highlight = { elements }; else - this._actionSelectorModel = null; + highlight = 'clear'; } - if (!state.actionSelector && !state.ariaTemplate) - this._actionSelectorModel = null; - - if (this.state.mode === 'none' || this.state.mode === 'standby') - this.updateHighlight(this._actionSelectorModel, false); + if (highlight === 'clear') + this.clearHighlight(); + else if (highlight !== 'noop') + this.updateHighlight(highlight, false); } clearHighlight() { @@ -1266,6 +1265,8 @@ export class Recorder { private _onScroll(event: Event) { if (!event.isTrusted) return; + this._lastHighlightedSelector = undefined; + this._lastHighlightedAriaTemplateJSON = 'undefined'; this.highlight.hideActionPoint(); this._currentTool.onScroll?.(event); } diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 50fc5174ecd52..f98682af89ebf 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -67,6 +67,7 @@ export const Recorder: React.FC = ({ const language = source.language; setLocator(asLocator(language, elementInfo.selector)); setAriaSnapshot(elementInfo.ariaSnapshot); + setAriaSnapshotErrors([]); if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria') setSelectedTab('locator'); @@ -122,9 +123,6 @@ export const Recorder: React.FC = ({ if (!errors.length) window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } }); }, [mode]); - const isRecording = mode === 'recording' || mode === 'recording-inspecting'; - const locatorPlaceholder = isRecording ? '// Unavailable while recording' : (locator ? undefined : '// Pick element or type locator'); - const ariaPlaceholder = isRecording ? '# Unavailable while recording' : (ariaSnapshot ? undefined : '# Pick element or type snapshot'); return
@@ -191,7 +189,7 @@ export const Recorder: React.FC = ({ { id: 'locator', title: 'Locator', - render: () => + render: () => }, { id: 'log', @@ -201,7 +199,7 @@ export const Recorder: React.FC = ({ { id: 'aria', title: 'Aria snapshot', - render: () => + render: () => }, ]} selectedTab={selectedTab} diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 528b4fcec8c3c..6050d4e64525d 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -31,8 +31,9 @@ type Fixtures = { }; const test = baseTest.extend({ - wsEndpoint: async ({ }, use) => { - process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1'; + wsEndpoint: async ({ headless }, use) => { + if (headless) + process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1'; const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false }); const wsEndpoint = await server.listen(); await use(wsEndpoint); diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts index 6fe75209eb13c..820dab7c14137 100644 --- a/tests/library/inspector/cli-codegen-aria.spec.ts +++ b/tests/library/inspector/cli-codegen-aria.spec.ts @@ -64,11 +64,10 @@ test.describe(() => { test('should inspect aria snapshot', async ({ openRecorder }) => { const { recorder } = await openRecorder(); await recorder.setContentAndWait(`
`); - await recorder.recorderPage.getByRole('button', { name: 'Record' }).click(); await recorder.page.click('x-pw-tool-item.pick-locator'); await recorder.page.hover('button'); await recorder.trustedClick(); - await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click(); + await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot' }).click(); await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(` - textbox - text: '- button "Submit"' @@ -85,12 +84,11 @@ test.describe(() => { const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' }); - await recorder.recorderPage.getByRole('button', { name: 'Record' }).click(); await recorder.page.click('x-pw-tool-item.pick-locator'); await submitButton.hover(); await recorder.trustedClick(); - await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click(); + await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot' }).click(); await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(` - text: '- button "Submit"' `); @@ -128,13 +126,12 @@ test.describe(() => { `); const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); - await recorder.recorderPage.getByRole('button', { name: 'Record' }).click(); await recorder.page.click('x-pw-tool-item.pick-locator'); await submitButton.hover(); await recorder.trustedClick(); - await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click(); + await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot' }).click(); await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(` - text: '- button "Submit"' `); diff --git a/tests/library/inspector/cli-codegen-pick-locator.spec.ts b/tests/library/inspector/cli-codegen-pick-locator.spec.ts new file mode 100644 index 0000000000000..53d106b4c9a1e --- /dev/null +++ b/tests/library/inspector/cli-codegen-pick-locator.spec.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './inspectorTest'; +import { roundBox } from '../../page/pageTest'; + +test.describe(() => { + test.skip(({ mode }) => mode !== 'default'); + test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events'); + + test('should inspect locator', async ({ openRecorder }) => { + const { recorder } = await openRecorder(); + await recorder.setContentAndWait(`
`); + await recorder.page.click('x-pw-tool-item.pick-locator'); + await recorder.page.hover('button'); + await recorder.trustedClick(); + await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click(); + await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(` + - text: "getByRole('button', { name: 'Submit' })" + `); + }); + + test('should update locator highlight', async ({ openRecorder }) => { + const { recorder } = await openRecorder(); + await recorder.setContentAndWait(`
+ + +
`); + + const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); + const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' }); + + await recorder.recorderPage.getByRole('button', { name: 'Record' }).click(); + + await recorder.page.click('x-pw-tool-item.pick-locator'); + await submitButton.hover(); + await recorder.trustedClick(); + await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click(); + await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(` + - text: "getByRole('button', { name: 'Submit' })" + `); + + await recorder.recorderPage.locator('.tab-locator .CodeMirror').click(); + for (let i = 0; i < `Submit' })`.length; i++) + await recorder.recorderPage.keyboard.press('Backspace'); + + { + // Different button. + await recorder.recorderPage.locator('.tab-locator .CodeMirror').pressSequentially(`Cancel' })`); + await expect(recorder.page.locator('x-pw-highlight')).toBeVisible(); + const box1 = roundBox(await cancelButton.boundingBox()); + const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox()); + expect(box1).toEqual(box2); + } + }); +});