chore: implement locator._highlight / playwright._hideHighlight (#11339)

This commit is contained in:
Pavel Feldman 2022-01-12 07:37:48 -08:00 committed by GitHub
parent bd837b5863
commit a12e76b52b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 316 additions and 169 deletions

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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': {

View file

@ -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 });

View file

@ -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 {

View file

@ -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,

View file

@ -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

View file

@ -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),

View file

@ -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')

View file

@ -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;

View 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;
}
}

View file

@ -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__';

View file

@ -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);
}
};
},
});

View file

@ -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 {

View file

@ -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) {

View file

@ -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;

View file

@ -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);
}

View file

@ -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) {

View file

@ -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]);
}

View file

@ -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)