diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index ad5eef8494..82257fa0cc 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -442,6 +442,10 @@ Attribute name to get the value for. ### option: Locator.getAttribute.timeout = %%-input-timeout-%% +## async method: Locator.highlight + +Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses [`method: Locator.highlight`]. + ## async method: Locator.hover This method hovers over the element by performing the following steps: diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 34bf7c6622..d0b5d2a56f 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -113,6 +113,11 @@ export class Locator implements api.Locator { } async _highlight() { + // VS Code extension uses this one, keep it for now. + return this._frame._highlight(this._selector); + } + + async highlight() { return this._frame._highlight(this._selector); } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index aab15dbf27..62d8f44564 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -28,6 +28,7 @@ import { Progress, ProgressController } from './progress'; import { SelectorInfo } from './selectors'; import * as types from './types'; import { TimeoutOptions } from '../common/types'; +import { isUnderTest } from '../utils/utils'; type SetInputFilesFiles = channels.ElementHandleSetInputFilesParams['files']; type ActionName = 'click' | 'hover' | 'dblclick' | 'tap' | 'move and up' | 'move and down'; @@ -99,6 +100,7 @@ export class FrameExecutionContext extends js.ExecutionContext { (() => { ${injectedScriptSource.source} return new pwExport( + ${isUnderTest()}, ${this.frame._page._delegate.rafCountForStablePosition()}, "${this.frame._page._browserContext._browser.options.name}", [${custom.join(',\n')}] diff --git a/packages/playwright-core/src/server/injected/highlight.ts b/packages/playwright-core/src/server/injected/highlight.ts index 631fd716f6..3031d95d0e 100644 --- a/packages/playwright-core/src/server/injected/highlight.ts +++ b/packages/playwright-core/src/server/injected/highlight.ts @@ -44,7 +44,7 @@ export class Highlight { this._innerGlassPaneElement.appendChild(this._tooltipElement); // Use a closed shadow root to prevent selectors matching our internal previews. - this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' }); + this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: isUnderTest ? 'open' : 'closed' }); this._glassPaneShadow.appendChild(this._innerGlassPaneElement); this._glassPaneShadow.appendChild(this._actionPointElement); const styleElement = document.createElement('style'); diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index b9d92e3680..41fa4bdd80 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -76,8 +76,10 @@ export class InjectedScript { onGlobalListenersRemoved = new Set<() => void>(); private _hitTargetInterceptor: undefined | ((event: MouseEvent | PointerEvent | TouchEvent) => void); private _highlight: Highlight | undefined; + readonly isUnderTest: boolean; - constructor(stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) { + constructor(isUnderTest: boolean, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) { + this.isUnderTest = isUnderTest; this._evaluator = new SelectorEvaluatorImpl(new Map()); this._engines = new Map(); @@ -876,7 +878,7 @@ export class InjectedScript { maskSelectors(selectors: ParsedSelector[]) { if (this._highlight) this.hideHighlight(); - this._highlight = new Highlight(false); + this._highlight = new Highlight(this.isUnderTest); this._highlight.install(); const elements = []; for (const selector of selectors) @@ -886,7 +888,7 @@ export class InjectedScript { highlight(selector: ParsedSelector) { if (!this._highlight) { - this._highlight = new Highlight(false); + this._highlight = new Highlight(this.isUnderTest); this._highlight.install(); } this._runHighlightOnRaf(selector); diff --git a/packages/playwright-core/src/server/supplements/injected/recorder.ts b/packages/playwright-core/src/server/supplements/injected/recorder.ts index 7eac90a378..88cc6aa48a 100644 --- a/packages/playwright-core/src/server/supplements/injected/recorder.ts +++ b/packages/playwright-core/src/server/supplements/injected/recorder.ts @@ -42,13 +42,11 @@ export class Recorder { private _mode: 'none' | 'inspecting' | 'recording' = 'none'; private _actionPoint: Point | undefined; private _actionSelector: string | undefined; - private _params: { isUnderTest: boolean; }; private _highlight: Highlight; - constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) { - this._params = params; + constructor(injectedScript: InjectedScript) { this._injectedScript = injectedScript; - this._highlight = new Highlight(params.isUnderTest); + this._highlight = new Highlight(injectedScript.isUnderTest); this._refreshListenersIfNeeded(); injectedScript.onGlobalListenersRemoved.add(() => this._refreshListenersIfNeeded()); @@ -57,7 +55,7 @@ export class Recorder { this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console }; globalThis._playwrightRefreshOverlay(); - if (params.isUnderTest) + if (injectedScript.isUnderTest) console.error('Recorder script ready for test'); // eslint-disable-line no-console } @@ -239,7 +237,7 @@ export class Recorder { const activeElement = this._deepActiveElement(document); const result = activeElement ? generateSelector(this._injectedScript, activeElement, true) : null; this._activeModel = result && result.selector ? result : null; - if (this._params.isUnderTest) + if (this._injectedScript.isUnderTest) console.error('Highlight updated for test: ' + (result ? result.selector : null)); // eslint-disable-line no-console } @@ -255,7 +253,7 @@ export class Recorder { return; this._hoveredModel = selector ? { selector, elements } : null; this._updateHighlight(); - if (this._params.isUnderTest) + if (this._injectedScript.isUnderTest) console.error('Highlight updated for test: ' + selector); // eslint-disable-line no-console } @@ -388,6 +386,7 @@ export class Recorder { } private async _performAction(action: actions.Action) { + this._clearHighlight(); this._performingAction = true; await globalThis._playwrightRecorderPerformAction(action).catch(() => {}); this._performingAction = false; @@ -397,7 +396,7 @@ export class Recorder { // If that was a keyboard action, it similarly requires new selectors for active model. this._onFocus(); - if (this._params.isUnderTest) { + if (this._injectedScript.isUnderTest) { // Serialize all to string as we cannot attribute console message to isolated world // in Firefox. console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console diff --git a/packages/playwright-core/src/server/supplements/recorderSupplement.ts b/packages/playwright-core/src/server/supplements/recorderSupplement.ts index 3f492f1b58..95b9e74459 100644 --- a/packages/playwright-core/src/server/supplements/recorderSupplement.ts +++ b/packages/playwright-core/src/server/supplements/recorderSupplement.ts @@ -32,7 +32,7 @@ import { IRecorderApp, RecorderApp } from './recorder/recorderApp'; import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation'; import { Point } from '../../common/types'; import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; -import { createGuid, isUnderTest, monotonicTime } from '../../utils/utils'; +import { createGuid, monotonicTime } from '../../utils/utils'; import { metadataToCallLog } from './recorder/recorderUtils'; import { Debugger } from './debugger'; import { EventEmitter } from 'events'; @@ -362,7 +362,7 @@ class ContextRecorder extends EventEmitter { await this._context.exposeBinding('_playwrightRecorderRecordAction', false, (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); - await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest() }); + await this._context.extendInjectedScript(recorderSource.source); } setEnabled(enabled: boolean) { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index c26b4236a1..019db99392 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9107,6 +9107,12 @@ export interface Locator { timeout?: number; }): Promise; + /** + * Highlight the corresponding element(s) on the screen. Useful for debugging, don't commit the code that uses + * [locator.highlight()](https://playwright.dev/docs/api/class-locator#locator-highlight). + */ + highlight(): Promise; + /** * This method hovers over the element by performing the following steps: * 1. Wait for [actionability](https://playwright.dev/docs/actionability) checks on the element, unless `force` option is set. diff --git a/tests/page/locator-highlight.spec.ts b/tests/page/locator-highlight.spec.ts new file mode 100644 index 0000000000..caf41a5a8a --- /dev/null +++ b/tests/page/locator-highlight.spec.ts @@ -0,0 +1,27 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * 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 as it, expect } from './pageTest'; + +it('should highlight locator', async ({ page }) => { + await page.setContent(``); + await page.locator('input').highlight(); + await expect(page.locator('x-pw-tooltip')).toHaveText('input'); + await expect(page.locator('x-pw-highlight')).toBeVisible(); + const box1 = await page.locator('input').boundingBox(); + const box2 = await page.locator('x-pw-highlight').boundingBox(); + expect(box1).toEqual(box2); +});