chore: implement locator._highlight / playwright._hideHighlight (#11339)
This commit is contained in:
parent
bd837b5863
commit
a12e76b52b
|
|
@ -284,6 +284,10 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
return await this._channel.fill({ selector, value, ...options });
|
||||
}
|
||||
|
||||
async _highlight(selector: string) {
|
||||
return await this._channel.highlight({ selector });
|
||||
}
|
||||
|
||||
locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
|
||||
return new Locator(this, selector, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@ export class Locator implements api.Locator {
|
|||
return this._frame.fill(this._selector, value, { strict: true, ...options });
|
||||
}
|
||||
|
||||
async _highlight() {
|
||||
return this._frame._highlight(this._selector);
|
||||
}
|
||||
|
||||
locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
|
||||
return new Locator(this._frame, this._selector + ' >> ' + selector, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
|||
for (const uid of this._sockets.keys())
|
||||
this._onSocksClosed(uid);
|
||||
});
|
||||
(global as any)._playwrightInstance = this;
|
||||
}
|
||||
|
||||
async _hideHighlight() {
|
||||
await this._channel.hideHighlight();
|
||||
}
|
||||
|
||||
_setSelectors(selectors: Selectors) {
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export class DispatcherConnection {
|
|||
} case 'log': {
|
||||
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
||||
originalMetadata.log.push(info.message);
|
||||
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
|
||||
sdkObject.instrumentation.onCallLog(sdkObject, originalMetadata, 'api', info.message);
|
||||
this.onmessage({ id });
|
||||
return;
|
||||
} case 'after': {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,10 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel> im
|
|||
return { value: await this._frame.title() };
|
||||
}
|
||||
|
||||
async highlight(params: channels.FrameHighlightParams, metadata: CallMetadata): Promise<void> {
|
||||
return await this._frame.highlight(params.selector);
|
||||
}
|
||||
|
||||
async expect(params: channels.FrameExpectParams, metadata: CallMetadata): Promise<channels.FrameExpectResult> {
|
||||
const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
|
||||
const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue });
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
|||
const request = new GlobalAPIRequestContext(this._object, params);
|
||||
return { request: APIRequestContextDispatcher.from(this._scope, request) };
|
||||
}
|
||||
|
||||
async hideHighlight(params: channels.PlaywrightHideHighlightParams, metadata?: channels.Metadata): Promise<channels.PlaywrightHideHighlightResult> {
|
||||
await this._object.hideHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
class SocksProxy implements SocksConnectionClient {
|
||||
|
|
|
|||
|
|
@ -432,6 +432,7 @@ export interface PlaywrightChannel extends PlaywrightEventTarget, Channel {
|
|||
socksError(params: PlaywrightSocksErrorParams, metadata?: Metadata): Promise<PlaywrightSocksErrorResult>;
|
||||
socksEnd(params: PlaywrightSocksEndParams, metadata?: Metadata): Promise<PlaywrightSocksEndResult>;
|
||||
newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>;
|
||||
hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise<PlaywrightHideHighlightResult>;
|
||||
}
|
||||
export type PlaywrightSocksRequestedEvent = {
|
||||
uid: string,
|
||||
|
|
@ -530,6 +531,9 @@ export type PlaywrightNewRequestOptions = {
|
|||
export type PlaywrightNewRequestResult = {
|
||||
request: APIRequestContextChannel,
|
||||
};
|
||||
export type PlaywrightHideHighlightParams = {};
|
||||
export type PlaywrightHideHighlightOptions = {};
|
||||
export type PlaywrightHideHighlightResult = void;
|
||||
|
||||
export interface PlaywrightEvents {
|
||||
'socksRequested': PlaywrightSocksRequestedEvent;
|
||||
|
|
@ -1779,6 +1783,7 @@ export interface FrameChannel extends FrameEventTarget, Channel {
|
|||
fill(params: FrameFillParams, metadata?: Metadata): Promise<FrameFillResult>;
|
||||
focus(params: FrameFocusParams, metadata?: Metadata): Promise<FrameFocusResult>;
|
||||
frameElement(params?: FrameFrameElementParams, metadata?: Metadata): Promise<FrameFrameElementResult>;
|
||||
highlight(params: FrameHighlightParams, metadata?: Metadata): Promise<FrameHighlightResult>;
|
||||
getAttribute(params: FrameGetAttributeParams, metadata?: Metadata): Promise<FrameGetAttributeResult>;
|
||||
goto(params: FrameGotoParams, metadata?: Metadata): Promise<FrameGotoResult>;
|
||||
hover(params: FrameHoverParams, metadata?: Metadata): Promise<FrameHoverResult>;
|
||||
|
|
@ -2028,6 +2033,13 @@ export type FrameFrameElementOptions = {};
|
|||
export type FrameFrameElementResult = {
|
||||
element: ElementHandleChannel,
|
||||
};
|
||||
export type FrameHighlightParams = {
|
||||
selector: string,
|
||||
};
|
||||
export type FrameHighlightOptions = {
|
||||
|
||||
};
|
||||
export type FrameHighlightResult = void;
|
||||
export type FrameGetAttributeParams = {
|
||||
selector: string,
|
||||
strict?: boolean,
|
||||
|
|
|
|||
|
|
@ -542,6 +542,8 @@ Playwright:
|
|||
returns:
|
||||
request: APIRequestContext
|
||||
|
||||
hideHighlight:
|
||||
|
||||
events:
|
||||
socksRequested:
|
||||
parameters:
|
||||
|
|
@ -1482,6 +1484,10 @@ Frame:
|
|||
returns:
|
||||
element: ElementHandle
|
||||
|
||||
highlight:
|
||||
parameters:
|
||||
selector: string
|
||||
|
||||
getAttribute:
|
||||
parameters:
|
||||
selector: string
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
origins: tArray(tType('OriginStorage')),
|
||||
})),
|
||||
});
|
||||
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
|
||||
scheme.SelectorsRegisterParams = tObject({
|
||||
name: tString,
|
||||
source: tString,
|
||||
|
|
@ -747,6 +748,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
timeout: tOptional(tNumber),
|
||||
});
|
||||
scheme.FrameFrameElementParams = tOptional(tObject({}));
|
||||
scheme.FrameHighlightParams = tObject({
|
||||
selector: tString,
|
||||
});
|
||||
scheme.FrameGetAttributeParams = tObject({
|
||||
selector: tString,
|
||||
strict: tOptional(tBoolean),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { Progress } from './progress';
|
|||
import { Selectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import path from 'path';
|
||||
import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
||||
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
|
||||
import { Debugger } from './supplements/debugger';
|
||||
import { Tracing } from './trace/recorder/tracing';
|
||||
import { HarRecorder } from './supplements/har/harRecorder';
|
||||
|
|
@ -76,9 +76,6 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this._isPersistentContext = !browserContextId;
|
||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||
|
||||
// Create instrumentation per context.
|
||||
this.instrumentation = createInstrumentation();
|
||||
|
||||
this.fetchRequest = new BrowserContextAPIRequestContext(this);
|
||||
|
||||
if (this._options.recordHar)
|
||||
|
|
@ -104,7 +101,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
return;
|
||||
// Debugger will pause execution upon page.pause in headed mode.
|
||||
const contextDebugger = new Debugger(this);
|
||||
this.instrumentation.addListener(contextDebugger);
|
||||
this.instrumentation.addListener(contextDebugger, this);
|
||||
|
||||
// When PWDEBUG=1, show inspector for each context.
|
||||
if (debugMode() === 'inspector')
|
||||
|
|
|
|||
|
|
@ -1140,6 +1140,25 @@ export class Frame extends SdkObject {
|
|||
}, undefined, options);
|
||||
}
|
||||
|
||||
async highlight(selector: string) {
|
||||
const pair = await this.resolveFrameForSelectorNoWait(selector);
|
||||
if (!pair)
|
||||
return;
|
||||
const context = await this._utilityContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
return await injectedScript.evaluate((injected, { parsed }) => {
|
||||
return injected.highlight(parsed);
|
||||
}, { parsed: pair.info.parsed });
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
const context = await this._utilityContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
return await injectedScript.evaluate(injected => {
|
||||
return injected.hideHighlight();
|
||||
});
|
||||
}
|
||||
|
||||
private async _elementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||
const result = await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => {
|
||||
const injected = progress.injectedScript;
|
||||
|
|
|
|||
184
packages/playwright-core/src/server/injected/highlight.ts
Normal file
184
packages/playwright-core/src/server/injected/highlight.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export class Highlight {
|
||||
private _outerGlassPaneElement: HTMLElement;
|
||||
private _glassPaneShadow: ShadowRoot;
|
||||
private _innerGlassPaneElement: HTMLElement;
|
||||
private _highlightElements: HTMLElement[] = [];
|
||||
private _tooltipElement: HTMLElement;
|
||||
private _actionPointElement: HTMLElement;
|
||||
private _isUnderTest: boolean;
|
||||
|
||||
constructor(isUnderTest: boolean) {
|
||||
this._isUnderTest = isUnderTest;
|
||||
this._outerGlassPaneElement = document.createElement('x-pw-glass');
|
||||
this._outerGlassPaneElement.style.position = 'fixed';
|
||||
this._outerGlassPaneElement.style.top = '0';
|
||||
this._outerGlassPaneElement.style.right = '0';
|
||||
this._outerGlassPaneElement.style.bottom = '0';
|
||||
this._outerGlassPaneElement.style.left = '0';
|
||||
this._outerGlassPaneElement.style.zIndex = '2147483647';
|
||||
this._outerGlassPaneElement.style.pointerEvents = 'none';
|
||||
this._outerGlassPaneElement.style.display = 'flex';
|
||||
|
||||
this._tooltipElement = document.createElement('x-pw-tooltip');
|
||||
this._actionPointElement = document.createElement('x-pw-action-point');
|
||||
this._actionPointElement.setAttribute('hidden', 'true');
|
||||
|
||||
this._innerGlassPaneElement = document.createElement('x-pw-glass-inner');
|
||||
this._innerGlassPaneElement.style.flex = 'auto';
|
||||
this._innerGlassPaneElement.appendChild(this._tooltipElement);
|
||||
|
||||
// Use a closed shadow root to prevent selectors matching our internal previews.
|
||||
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' });
|
||||
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
||||
this._glassPaneShadow.appendChild(this._actionPointElement);
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = `
|
||||
x-pw-tooltip {
|
||||
align-items: center;
|
||||
backdrop-filter: blur(5px);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 2px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 3.6px 3.7px,
|
||||
rgba(0, 0, 0, 0.15) 0px 12.1px 12.3px,
|
||||
rgba(0, 0, 0, 0.1) 0px -2px 4px,
|
||||
rgba(0, 0, 0, 0.15) 0px -12.1px 24px,
|
||||
rgba(0, 0, 0, 0.25) 0px 54px 55px;
|
||||
color: rgb(204, 204, 204);
|
||||
display: none;
|
||||
font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono',
|
||||
'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace;
|
||||
font-size: 12.8px;
|
||||
font-weight: normal;
|
||||
left: 0;
|
||||
line-height: 1.5;
|
||||
max-width: 600px;
|
||||
padding: 3.2px 5.12px 3.2px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
x-pw-action-point {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: red;
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
margin: -10px 0 0 -10px;
|
||||
z-index: 2;
|
||||
}
|
||||
*[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
this._glassPaneShadow.appendChild(styleElement);
|
||||
}
|
||||
|
||||
install() {
|
||||
document.documentElement.appendChild(this._outerGlassPaneElement);
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
this._outerGlassPaneElement.remove();
|
||||
}
|
||||
|
||||
isInstalled(): boolean {
|
||||
return this._outerGlassPaneElement.parentElement === document.documentElement && !this._outerGlassPaneElement.nextElementSibling;
|
||||
}
|
||||
|
||||
showActionPoint(x: number, y: number) {
|
||||
this._actionPointElement.style.top = y + 'px';
|
||||
this._actionPointElement.style.left = x + 'px';
|
||||
this._actionPointElement.hidden = false;
|
||||
}
|
||||
|
||||
hideActionPoint() {
|
||||
this._actionPointElement.hidden = true;
|
||||
}
|
||||
|
||||
updateHighlight(elements: Element[], selector: string, isRecording: boolean) {
|
||||
// Code below should trigger one layout and leave with the
|
||||
// destroyed layout.
|
||||
|
||||
// Destroy the layout
|
||||
this._tooltipElement.textContent = selector;
|
||||
this._tooltipElement.style.top = '0';
|
||||
this._tooltipElement.style.left = '0';
|
||||
this._tooltipElement.style.display = 'flex';
|
||||
|
||||
// Trigger layout.
|
||||
const boxes = elements.map(e => e.getBoundingClientRect());
|
||||
const tooltipWidth = this._tooltipElement.offsetWidth;
|
||||
const tooltipHeight = this._tooltipElement.offsetHeight;
|
||||
const totalWidth = this._innerGlassPaneElement.offsetWidth;
|
||||
const totalHeight = this._innerGlassPaneElement.offsetHeight;
|
||||
|
||||
// Destroy the layout again.
|
||||
if (boxes.length) {
|
||||
const primaryBox = boxes[0];
|
||||
let anchorLeft = primaryBox.left;
|
||||
if (anchorLeft + tooltipWidth > totalWidth - 5)
|
||||
anchorLeft = totalWidth - tooltipWidth - 5;
|
||||
let anchorTop = primaryBox.bottom + 5;
|
||||
if (anchorTop + tooltipHeight > totalHeight - 5) {
|
||||
// If can't fit below, either position above...
|
||||
if (primaryBox.top > tooltipHeight + 5) {
|
||||
anchorTop = primaryBox.top - tooltipHeight - 5;
|
||||
} else {
|
||||
// Or on top in case of large element
|
||||
anchorTop = totalHeight - 5 - tooltipHeight;
|
||||
}
|
||||
}
|
||||
this._tooltipElement.style.top = anchorTop + 'px';
|
||||
this._tooltipElement.style.left = anchorLeft + 'px';
|
||||
} else {
|
||||
this._tooltipElement.style.display = 'none';
|
||||
}
|
||||
|
||||
const pool = this._highlightElements;
|
||||
this._highlightElements = [];
|
||||
for (const box of boxes) {
|
||||
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement();
|
||||
const color = isRecording ? '#dc6f6f7f' : '#6fa8dc7f';
|
||||
highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : color;
|
||||
highlightElement.style.left = box.x + 'px';
|
||||
highlightElement.style.top = box.y + 'px';
|
||||
highlightElement.style.width = box.width + 'px';
|
||||
highlightElement.style.height = box.height + 'px';
|
||||
highlightElement.style.display = 'block';
|
||||
this._highlightElements.push(highlightElement);
|
||||
}
|
||||
|
||||
for (const highlightElement of pool) {
|
||||
highlightElement.style.display = 'none';
|
||||
this._highlightElements.push(highlightElement);
|
||||
}
|
||||
}
|
||||
|
||||
private _createHighlightElement(): HTMLElement {
|
||||
const highlightElement = document.createElement('x-pw-highlight');
|
||||
highlightElement.style.position = 'absolute';
|
||||
highlightElement.style.top = '0';
|
||||
highlightElement.style.left = '0';
|
||||
highlightElement.style.width = '0';
|
||||
highlightElement.style.height = '0';
|
||||
highlightElement.style.boxSizing = 'border-box';
|
||||
this._glassPaneShadow.appendChild(highlightElement);
|
||||
return highlightElement;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMat
|
|||
import { CSSComplexSelectorList } from '../common/cssParser';
|
||||
import { generateSelector } from './selectorGenerator';
|
||||
import type * as channels from '../../protocol/channels';
|
||||
import { Highlight } from './highlight';
|
||||
|
||||
type Predicate<T> = (progress: InjectedScriptProgress) => T | symbol;
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ export class InjectedScript {
|
|||
private _browserName: string;
|
||||
onGlobalListenersRemoved = new Set<() => void>();
|
||||
private _hitTargetInterceptor: undefined | ((event: MouseEvent | PointerEvent | TouchEvent) => void);
|
||||
private _highlight: Highlight | undefined;
|
||||
|
||||
constructor(stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) {
|
||||
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
||||
|
|
@ -856,6 +858,21 @@ export class InjectedScript {
|
|||
return error;
|
||||
}
|
||||
|
||||
highlight(selector: ParsedSelector) {
|
||||
if (!this._highlight) {
|
||||
this._highlight = new Highlight(false);
|
||||
this._highlight.install();
|
||||
}
|
||||
this._highlight.updateHighlight(this.querySelectorAll(selector, document.documentElement), stringifySelector(selector), false);
|
||||
}
|
||||
|
||||
hideHighlight() {
|
||||
if (this._highlight) {
|
||||
this._highlight.uninstall();
|
||||
delete this._highlight;
|
||||
}
|
||||
}
|
||||
|
||||
private _setupGlobalListenersRemovalDetection() {
|
||||
const customEventName = '__playwright_global_listeners_check__';
|
||||
|
||||
|
|
|
|||
|
|
@ -50,36 +50,42 @@ export class SdkObject extends EventEmitter {
|
|||
}
|
||||
|
||||
export interface Instrumentation {
|
||||
addListener(listener: InstrumentationListener): void;
|
||||
addListener(listener: InstrumentationListener, context: BrowserContext | null): void;
|
||||
removeListener(listener: InstrumentationListener): void;
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||
onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onPageOpen(page: Page): void;
|
||||
onPageClose(page: Page): void;
|
||||
}
|
||||
|
||||
export interface InstrumentationListener {
|
||||
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||
onCallLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onPageOpen?(page: Page): void;
|
||||
onPageClose?(page: Page): void;
|
||||
}
|
||||
|
||||
export function createInstrumentation(): Instrumentation {
|
||||
const listeners: InstrumentationListener[] = [];
|
||||
const listeners = new Map<InstrumentationListener, BrowserContext | null>();
|
||||
return new Proxy({}, {
|
||||
get: (obj: any, prop: string) => {
|
||||
if (prop === 'addListener')
|
||||
return (listener: InstrumentationListener) => listeners.push(listener);
|
||||
return (listener: InstrumentationListener, context: BrowserContext | null) => listeners.set(listener, context);
|
||||
if (prop === 'removeListener')
|
||||
return (listener: InstrumentationListener) => listeners.splice(listeners.indexOf(listener), 1);
|
||||
return (listener: InstrumentationListener) => listeners.delete(listener);
|
||||
if (!prop.startsWith('on'))
|
||||
return obj[prop];
|
||||
return async (...params: any[]) => {
|
||||
for (const listener of listeners)
|
||||
await (listener as any)[prop]?.(...params);
|
||||
return async (sdkObject: SdkObject, ...params: any[]) => {
|
||||
for (const [listener, context] of listeners) {
|
||||
if (!context || sdkObject.attribution.context === context)
|
||||
await (listener as any)[prop]?.(sdkObject, ...params);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ export class Page extends SdkObject {
|
|||
this.pdf = delegate.pdf.bind(delegate);
|
||||
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
||||
this.selectors = browserContext.selectors();
|
||||
this.instrumentation.onPageOpen(this);
|
||||
}
|
||||
|
||||
async initOpener(opener: PageDelegate | null) {
|
||||
|
|
@ -208,6 +209,7 @@ export class Page extends SdkObject {
|
|||
}
|
||||
|
||||
_didClose() {
|
||||
this.instrumentation.onPageClose(this);
|
||||
this._frameManager.dispose();
|
||||
this._frameThrottler.setEnabled(false);
|
||||
assert(this._closedState !== 'closed', 'Page closed twice');
|
||||
|
|
@ -217,6 +219,7 @@ export class Page extends SdkObject {
|
|||
}
|
||||
|
||||
_didCrash() {
|
||||
this.instrumentation.onPageClose(this);
|
||||
this._frameManager.dispose();
|
||||
this._frameThrottler.setEnabled(false);
|
||||
this.emit(Page.Events.Crash);
|
||||
|
|
@ -224,6 +227,7 @@ export class Page extends SdkObject {
|
|||
}
|
||||
|
||||
_didDisconnect() {
|
||||
this.instrumentation.onPageClose(this);
|
||||
this._frameManager.dispose();
|
||||
this._frameThrottler.setEnabled(false);
|
||||
assert(!this._disconnected, 'Page disconnected twice');
|
||||
|
|
@ -518,6 +522,10 @@ export class Page extends SdkObject {
|
|||
const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.context()._options.strictSelectors;
|
||||
return this.selectors.parseSelector(selector, strict);
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {})));
|
||||
}
|
||||
}
|
||||
|
||||
export class Worker extends SdkObject {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { Selectors } from './selectors';
|
|||
import { WebKit } from './webkit/webkit';
|
||||
import { CallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { Page } from './page';
|
||||
|
||||
export class Playwright extends SdkObject {
|
||||
readonly selectors: Selectors;
|
||||
|
|
@ -33,14 +34,17 @@ export class Playwright extends SdkObject {
|
|||
readonly firefox: Firefox;
|
||||
readonly webkit: WebKit;
|
||||
readonly options: PlaywrightOptions;
|
||||
private _allPages = new Set<Page>();
|
||||
|
||||
constructor(sdkLanguage: string, isInternal: boolean) {
|
||||
super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
|
||||
this.instrumentation.addListener({
|
||||
onCallLog: (logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata) => {
|
||||
onPageOpen: page => this._allPages.add(page),
|
||||
onPageClose: page => this._allPages.delete(page),
|
||||
onCallLog: (sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) => {
|
||||
debugLogger.log(logName as any, message);
|
||||
}
|
||||
});
|
||||
}, null);
|
||||
this.options = {
|
||||
rootSdkObject: this,
|
||||
selectors: new Selectors(),
|
||||
|
|
@ -53,6 +57,10 @@ export class Playwright extends SdkObject {
|
|||
this.android = new Android(new AdbBackend(), this.options);
|
||||
this.selectors = this.options.selectors;
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
await Promise.all([...this._allPages].map(p => p.hideHighlight().catch(() => {})));
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlaywright(sdkLanguage: string, isInternal: boolean = false) {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export class ProgressController {
|
|||
if (this._state === 'running')
|
||||
this.metadata.log.push(message);
|
||||
// Note: we might be sending logs after progress has finished, for example browser logs.
|
||||
this.instrumentation.onCallLog(this._logName, message, this.sdkObject, this.metadata);
|
||||
this.instrumentation.onCallLog(this.sdkObject, this.metadata, this._logName, message);
|
||||
}
|
||||
if ('intermediateResult' in entry)
|
||||
this._lastIntermediateResult = entry.intermediateResult;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
|
|||
await this.pause(sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
async onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): Promise<void> {
|
||||
debugLogger.log(logName as any, message);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type InjectedScript from '../../injected/injectedScript';
|
|||
import { generateSelector, querySelector } from '../../injected/selectorGenerator';
|
||||
import type { Point } from '../../../common/types';
|
||||
import type { UIState } from '../recorder/recorderTypes';
|
||||
import { Highlight } from '../../injected/highlight';
|
||||
|
||||
|
||||
declare module globalThis {
|
||||
|
|
@ -32,11 +33,6 @@ declare module globalThis {
|
|||
export class Recorder {
|
||||
private _injectedScript: InjectedScript;
|
||||
private _performingAction = false;
|
||||
private _outerGlassPaneElement: HTMLElement;
|
||||
private _glassPaneShadow: ShadowRoot;
|
||||
private _innerGlassPaneElement: HTMLElement;
|
||||
private _highlightElements: HTMLElement[] = [];
|
||||
private _tooltipElement: HTMLElement;
|
||||
private _listeners: (() => void)[] = [];
|
||||
private _hoveredModel: HighlightModel | null = null;
|
||||
private _hoveredElement: HTMLElement | null = null;
|
||||
|
|
@ -44,76 +40,15 @@ export class Recorder {
|
|||
private _expectProgrammaticKeyUp = false;
|
||||
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
||||
private _actionPointElement: HTMLElement;
|
||||
private _actionPoint: Point | undefined;
|
||||
private _actionSelector: string | undefined;
|
||||
private _params: { isUnderTest: boolean; };
|
||||
private _highlight: Highlight;
|
||||
|
||||
constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) {
|
||||
this._params = params;
|
||||
this._injectedScript = injectedScript;
|
||||
this._outerGlassPaneElement = document.createElement('x-pw-glass');
|
||||
this._outerGlassPaneElement.style.position = 'fixed';
|
||||
this._outerGlassPaneElement.style.top = '0';
|
||||
this._outerGlassPaneElement.style.right = '0';
|
||||
this._outerGlassPaneElement.style.bottom = '0';
|
||||
this._outerGlassPaneElement.style.left = '0';
|
||||
this._outerGlassPaneElement.style.zIndex = '2147483647';
|
||||
this._outerGlassPaneElement.style.pointerEvents = 'none';
|
||||
this._outerGlassPaneElement.style.display = 'flex';
|
||||
|
||||
this._tooltipElement = document.createElement('x-pw-tooltip');
|
||||
this._actionPointElement = document.createElement('x-pw-action-point');
|
||||
this._actionPointElement.setAttribute('hidden', 'true');
|
||||
|
||||
this._innerGlassPaneElement = document.createElement('x-pw-glass-inner');
|
||||
this._innerGlassPaneElement.style.flex = 'auto';
|
||||
this._innerGlassPaneElement.appendChild(this._tooltipElement);
|
||||
|
||||
// Use a closed shadow root to prevent selectors matching our internal previews.
|
||||
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: this._params.isUnderTest ? 'open' : 'closed' });
|
||||
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
||||
this._glassPaneShadow.appendChild(this._actionPointElement);
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = `
|
||||
x-pw-tooltip {
|
||||
align-items: center;
|
||||
backdrop-filter: blur(5px);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 2px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 3.6px 3.7px,
|
||||
rgba(0, 0, 0, 0.15) 0px 12.1px 12.3px,
|
||||
rgba(0, 0, 0, 0.1) 0px -2px 4px,
|
||||
rgba(0, 0, 0, 0.15) 0px -12.1px 24px,
|
||||
rgba(0, 0, 0, 0.25) 0px 54px 55px;
|
||||
color: rgb(204, 204, 204);
|
||||
display: none;
|
||||
font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono',
|
||||
'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace;
|
||||
font-size: 12.8px;
|
||||
font-weight: normal;
|
||||
left: 0;
|
||||
line-height: 1.5;
|
||||
max-width: 600px;
|
||||
padding: 3.2px 5.12px 3.2px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
x-pw-action-point {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: red;
|
||||
border-radius: 10px;
|
||||
pointer-events: none;
|
||||
margin: -10px 0 0 -10px;
|
||||
z-index: 2;
|
||||
}
|
||||
*[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
this._glassPaneShadow.appendChild(styleElement);
|
||||
this._highlight = new Highlight(params.isUnderTest);
|
||||
|
||||
this._refreshListenersIfNeeded();
|
||||
injectedScript.onGlobalListenersRemoved.add(() => this._refreshListenersIfNeeded());
|
||||
|
|
@ -128,7 +63,7 @@ export class Recorder {
|
|||
|
||||
private _refreshListenersIfNeeded() {
|
||||
// Ensure we are attached to the current document, and we are on top (last element);
|
||||
if (this._outerGlassPaneElement.parentElement === document.documentElement && !this._outerGlassPaneElement.nextElementSibling)
|
||||
if (this._highlight.isInstalled())
|
||||
return;
|
||||
removeEventListeners(this._listeners);
|
||||
this._listeners = [
|
||||
|
|
@ -144,11 +79,11 @@ export class Recorder {
|
|||
addEventListener(document, 'focus', () => this._onFocus(), true),
|
||||
addEventListener(document, 'scroll', () => {
|
||||
this._hoveredModel = null;
|
||||
this._actionPointElement.hidden = true;
|
||||
this._highlight.hideActionPoint();
|
||||
this._updateHighlight();
|
||||
}, true),
|
||||
];
|
||||
document.documentElement.appendChild(this._outerGlassPaneElement);
|
||||
this._highlight.install();
|
||||
}
|
||||
|
||||
private async _pollRecorderMode() {
|
||||
|
|
@ -171,13 +106,10 @@ export class Recorder {
|
|||
} else if (!actionPoint && !this._actionPoint) {
|
||||
// All good.
|
||||
} else {
|
||||
if (actionPoint) {
|
||||
this._actionPointElement.style.top = actionPoint.y + 'px';
|
||||
this._actionPointElement.style.left = actionPoint.x + 'px';
|
||||
this._actionPointElement.hidden = false;
|
||||
} else {
|
||||
this._actionPointElement.hidden = true;
|
||||
}
|
||||
if (actionPoint)
|
||||
this._highlight.showActionPoint(actionPoint.x, actionPoint.y);
|
||||
else
|
||||
this._highlight.hideActionPoint();
|
||||
this._actionPoint = actionPoint;
|
||||
}
|
||||
|
||||
|
|
@ -329,75 +261,8 @@ export class Recorder {
|
|||
|
||||
private _updateHighlight() {
|
||||
const elements = this._hoveredModel ? this._hoveredModel.elements : [];
|
||||
|
||||
// Code below should trigger one layout and leave with the
|
||||
// destroyed layout.
|
||||
|
||||
// Destroy the layout
|
||||
this._tooltipElement.textContent = this._hoveredModel ? this._hoveredModel.selector : '';
|
||||
this._tooltipElement.style.top = '0';
|
||||
this._tooltipElement.style.left = '0';
|
||||
this._tooltipElement.style.display = 'flex';
|
||||
|
||||
// Trigger layout.
|
||||
const boxes = elements.map(e => e.getBoundingClientRect());
|
||||
const tooltipWidth = this._tooltipElement.offsetWidth;
|
||||
const tooltipHeight = this._tooltipElement.offsetHeight;
|
||||
const totalWidth = this._innerGlassPaneElement.offsetWidth;
|
||||
const totalHeight = this._innerGlassPaneElement.offsetHeight;
|
||||
|
||||
// Destroy the layout again.
|
||||
if (boxes.length) {
|
||||
const primaryBox = boxes[0];
|
||||
let anchorLeft = primaryBox.left;
|
||||
if (anchorLeft + tooltipWidth > totalWidth - 5)
|
||||
anchorLeft = totalWidth - tooltipWidth - 5;
|
||||
let anchorTop = primaryBox.bottom + 5;
|
||||
if (anchorTop + tooltipHeight > totalHeight - 5) {
|
||||
// If can't fit below, either position above...
|
||||
if (primaryBox.top > tooltipHeight + 5) {
|
||||
anchorTop = primaryBox.top - tooltipHeight - 5;
|
||||
} else {
|
||||
// Or on top in case of large element
|
||||
anchorTop = totalHeight - 5 - tooltipHeight;
|
||||
}
|
||||
}
|
||||
this._tooltipElement.style.top = anchorTop + 'px';
|
||||
this._tooltipElement.style.left = anchorLeft + 'px';
|
||||
} else {
|
||||
this._tooltipElement.style.display = 'none';
|
||||
}
|
||||
|
||||
const pool = this._highlightElements;
|
||||
this._highlightElements = [];
|
||||
for (const box of boxes) {
|
||||
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement();
|
||||
const color = this._mode === 'recording' ? '#dc6f6f7f' : '#6fa8dc7f';
|
||||
highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : color;
|
||||
highlightElement.style.left = box.x + 'px';
|
||||
highlightElement.style.top = box.y + 'px';
|
||||
highlightElement.style.width = box.width + 'px';
|
||||
highlightElement.style.height = box.height + 'px';
|
||||
highlightElement.style.display = 'block';
|
||||
this._highlightElements.push(highlightElement);
|
||||
}
|
||||
|
||||
for (const highlightElement of pool) {
|
||||
highlightElement.style.display = 'none';
|
||||
this._highlightElements.push(highlightElement);
|
||||
}
|
||||
}
|
||||
|
||||
private _createHighlightElement(): HTMLElement {
|
||||
const highlightElement = document.createElement('x-pw-highlight');
|
||||
highlightElement.style.position = 'absolute';
|
||||
highlightElement.style.top = '0';
|
||||
highlightElement.style.left = '0';
|
||||
highlightElement.style.width = '0';
|
||||
highlightElement.style.height = '0';
|
||||
highlightElement.style.boxSizing = 'border-box';
|
||||
this._glassPaneShadow.appendChild(highlightElement);
|
||||
return highlightElement;
|
||||
const selector = this._hoveredModel ? this._hoveredModel.selector : '';
|
||||
this._highlight.updateHighlight(elements, selector, this._mode === 'recording');
|
||||
}
|
||||
|
||||
private _onInput(event: Event) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export class RecorderSupplement implements InstrumentationListener {
|
|||
this._contextRecorder = new ContextRecorder(context, params);
|
||||
this._context = context;
|
||||
this._debugger = Debugger.lookup(context)!;
|
||||
context.instrumentation.addListener(this);
|
||||
context.instrumentation.addListener(this, context);
|
||||
}
|
||||
|
||||
async install() {
|
||||
|
|
@ -248,7 +248,7 @@ export class RecorderSupplement implements InstrumentationListener {
|
|||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
}
|
||||
|
||||
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
async onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): Promise<void> {
|
||||
this.updateCallLog([metadata]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
|
|||
await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n');
|
||||
});
|
||||
|
||||
this._context.instrumentation.addListener(this);
|
||||
this._context.instrumentation.addListener(this, this._context);
|
||||
if (state.options.screenshots)
|
||||
this._startScreencast();
|
||||
if (state.options.snapshots)
|
||||
|
|
|
|||
Loading…
Reference in a new issue