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 });
|
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 {
|
locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
|
||||||
return new Locator(this, selector, options);
|
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 });
|
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 {
|
locator(selector: string, options?: { hasText?: string | RegExp }): Locator {
|
||||||
return new Locator(this._frame, this._selector + ' >> ' + selector, options);
|
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())
|
for (const uid of this._sockets.keys())
|
||||||
this._onSocksClosed(uid);
|
this._onSocksClosed(uid);
|
||||||
});
|
});
|
||||||
|
(global as any)._playwrightInstance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _hideHighlight() {
|
||||||
|
await this._channel.hideHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
_setSelectors(selectors: Selectors) {
|
_setSelectors(selectors: Selectors) {
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ export class DispatcherConnection {
|
||||||
} case 'log': {
|
} case 'log': {
|
||||||
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
||||||
originalMetadata.log.push(info.message);
|
originalMetadata.log.push(info.message);
|
||||||
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
|
sdkObject.instrumentation.onCallLog(sdkObject, originalMetadata, 'api', info.message);
|
||||||
this.onmessage({ id });
|
this.onmessage({ id });
|
||||||
return;
|
return;
|
||||||
} case 'after': {
|
} case 'after': {
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,10 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel> im
|
||||||
return { value: await this._frame.title() };
|
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> {
|
async expect(params: channels.FrameExpectParams, metadata: CallMetadata): Promise<channels.FrameExpectResult> {
|
||||||
const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
|
const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
|
||||||
const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue });
|
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);
|
const request = new GlobalAPIRequestContext(this._object, params);
|
||||||
return { request: APIRequestContextDispatcher.from(this._scope, request) };
|
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 {
|
class SocksProxy implements SocksConnectionClient {
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,7 @@ export interface PlaywrightChannel extends PlaywrightEventTarget, Channel {
|
||||||
socksError(params: PlaywrightSocksErrorParams, metadata?: Metadata): Promise<PlaywrightSocksErrorResult>;
|
socksError(params: PlaywrightSocksErrorParams, metadata?: Metadata): Promise<PlaywrightSocksErrorResult>;
|
||||||
socksEnd(params: PlaywrightSocksEndParams, metadata?: Metadata): Promise<PlaywrightSocksEndResult>;
|
socksEnd(params: PlaywrightSocksEndParams, metadata?: Metadata): Promise<PlaywrightSocksEndResult>;
|
||||||
newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>;
|
newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise<PlaywrightNewRequestResult>;
|
||||||
|
hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise<PlaywrightHideHighlightResult>;
|
||||||
}
|
}
|
||||||
export type PlaywrightSocksRequestedEvent = {
|
export type PlaywrightSocksRequestedEvent = {
|
||||||
uid: string,
|
uid: string,
|
||||||
|
|
@ -530,6 +531,9 @@ export type PlaywrightNewRequestOptions = {
|
||||||
export type PlaywrightNewRequestResult = {
|
export type PlaywrightNewRequestResult = {
|
||||||
request: APIRequestContextChannel,
|
request: APIRequestContextChannel,
|
||||||
};
|
};
|
||||||
|
export type PlaywrightHideHighlightParams = {};
|
||||||
|
export type PlaywrightHideHighlightOptions = {};
|
||||||
|
export type PlaywrightHideHighlightResult = void;
|
||||||
|
|
||||||
export interface PlaywrightEvents {
|
export interface PlaywrightEvents {
|
||||||
'socksRequested': PlaywrightSocksRequestedEvent;
|
'socksRequested': PlaywrightSocksRequestedEvent;
|
||||||
|
|
@ -1779,6 +1783,7 @@ export interface FrameChannel extends FrameEventTarget, Channel {
|
||||||
fill(params: FrameFillParams, metadata?: Metadata): Promise<FrameFillResult>;
|
fill(params: FrameFillParams, metadata?: Metadata): Promise<FrameFillResult>;
|
||||||
focus(params: FrameFocusParams, metadata?: Metadata): Promise<FrameFocusResult>;
|
focus(params: FrameFocusParams, metadata?: Metadata): Promise<FrameFocusResult>;
|
||||||
frameElement(params?: FrameFrameElementParams, metadata?: Metadata): Promise<FrameFrameElementResult>;
|
frameElement(params?: FrameFrameElementParams, metadata?: Metadata): Promise<FrameFrameElementResult>;
|
||||||
|
highlight(params: FrameHighlightParams, metadata?: Metadata): Promise<FrameHighlightResult>;
|
||||||
getAttribute(params: FrameGetAttributeParams, metadata?: Metadata): Promise<FrameGetAttributeResult>;
|
getAttribute(params: FrameGetAttributeParams, metadata?: Metadata): Promise<FrameGetAttributeResult>;
|
||||||
goto(params: FrameGotoParams, metadata?: Metadata): Promise<FrameGotoResult>;
|
goto(params: FrameGotoParams, metadata?: Metadata): Promise<FrameGotoResult>;
|
||||||
hover(params: FrameHoverParams, metadata?: Metadata): Promise<FrameHoverResult>;
|
hover(params: FrameHoverParams, metadata?: Metadata): Promise<FrameHoverResult>;
|
||||||
|
|
@ -2028,6 +2033,13 @@ export type FrameFrameElementOptions = {};
|
||||||
export type FrameFrameElementResult = {
|
export type FrameFrameElementResult = {
|
||||||
element: ElementHandleChannel,
|
element: ElementHandleChannel,
|
||||||
};
|
};
|
||||||
|
export type FrameHighlightParams = {
|
||||||
|
selector: string,
|
||||||
|
};
|
||||||
|
export type FrameHighlightOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type FrameHighlightResult = void;
|
||||||
export type FrameGetAttributeParams = {
|
export type FrameGetAttributeParams = {
|
||||||
selector: string,
|
selector: string,
|
||||||
strict?: boolean,
|
strict?: boolean,
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,8 @@ Playwright:
|
||||||
returns:
|
returns:
|
||||||
request: APIRequestContext
|
request: APIRequestContext
|
||||||
|
|
||||||
|
hideHighlight:
|
||||||
|
|
||||||
events:
|
events:
|
||||||
socksRequested:
|
socksRequested:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
@ -1482,6 +1484,10 @@ Frame:
|
||||||
returns:
|
returns:
|
||||||
element: ElementHandle
|
element: ElementHandle
|
||||||
|
|
||||||
|
highlight:
|
||||||
|
parameters:
|
||||||
|
selector: string
|
||||||
|
|
||||||
getAttribute:
|
getAttribute:
|
||||||
parameters:
|
parameters:
|
||||||
selector: string
|
selector: string
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
origins: tArray(tType('OriginStorage')),
|
origins: tArray(tType('OriginStorage')),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
scheme.PlaywrightHideHighlightParams = tOptional(tObject({}));
|
||||||
scheme.SelectorsRegisterParams = tObject({
|
scheme.SelectorsRegisterParams = tObject({
|
||||||
name: tString,
|
name: tString,
|
||||||
source: tString,
|
source: tString,
|
||||||
|
|
@ -747,6 +748,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
scheme.FrameFrameElementParams = tOptional(tObject({}));
|
scheme.FrameFrameElementParams = tOptional(tObject({}));
|
||||||
|
scheme.FrameHighlightParams = tObject({
|
||||||
|
selector: tString,
|
||||||
|
});
|
||||||
scheme.FrameGetAttributeParams = tObject({
|
scheme.FrameGetAttributeParams = tObject({
|
||||||
selector: tString,
|
selector: tString,
|
||||||
strict: tOptional(tBoolean),
|
strict: tOptional(tBoolean),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { Progress } from './progress';
|
||||||
import { Selectors } from './selectors';
|
import { Selectors } from './selectors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
|
||||||
import { Debugger } from './supplements/debugger';
|
import { Debugger } from './supplements/debugger';
|
||||||
import { Tracing } from './trace/recorder/tracing';
|
import { Tracing } from './trace/recorder/tracing';
|
||||||
import { HarRecorder } from './supplements/har/harRecorder';
|
import { HarRecorder } from './supplements/har/harRecorder';
|
||||||
|
|
@ -76,9 +76,6 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
this._isPersistentContext = !browserContextId;
|
this._isPersistentContext = !browserContextId;
|
||||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||||
|
|
||||||
// Create instrumentation per context.
|
|
||||||
this.instrumentation = createInstrumentation();
|
|
||||||
|
|
||||||
this.fetchRequest = new BrowserContextAPIRequestContext(this);
|
this.fetchRequest = new BrowserContextAPIRequestContext(this);
|
||||||
|
|
||||||
if (this._options.recordHar)
|
if (this._options.recordHar)
|
||||||
|
|
@ -104,7 +101,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
return;
|
return;
|
||||||
// Debugger will pause execution upon page.pause in headed mode.
|
// Debugger will pause execution upon page.pause in headed mode.
|
||||||
const contextDebugger = new Debugger(this);
|
const contextDebugger = new Debugger(this);
|
||||||
this.instrumentation.addListener(contextDebugger);
|
this.instrumentation.addListener(contextDebugger, this);
|
||||||
|
|
||||||
// When PWDEBUG=1, show inspector for each context.
|
// When PWDEBUG=1, show inspector for each context.
|
||||||
if (debugMode() === 'inspector')
|
if (debugMode() === 'inspector')
|
||||||
|
|
|
||||||
|
|
@ -1140,6 +1140,25 @@ export class Frame extends SdkObject {
|
||||||
}, undefined, options);
|
}, 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> {
|
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 result = await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => {
|
||||||
const injected = progress.injectedScript;
|
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 { CSSComplexSelectorList } from '../common/cssParser';
|
||||||
import { generateSelector } from './selectorGenerator';
|
import { generateSelector } from './selectorGenerator';
|
||||||
import type * as channels from '../../protocol/channels';
|
import type * as channels from '../../protocol/channels';
|
||||||
|
import { Highlight } from './highlight';
|
||||||
|
|
||||||
type Predicate<T> = (progress: InjectedScriptProgress) => T | symbol;
|
type Predicate<T> = (progress: InjectedScriptProgress) => T | symbol;
|
||||||
|
|
||||||
|
|
@ -74,6 +75,7 @@ export class InjectedScript {
|
||||||
private _browserName: string;
|
private _browserName: string;
|
||||||
onGlobalListenersRemoved = new Set<() => void>();
|
onGlobalListenersRemoved = new Set<() => void>();
|
||||||
private _hitTargetInterceptor: undefined | ((event: MouseEvent | PointerEvent | TouchEvent) => void);
|
private _hitTargetInterceptor: undefined | ((event: MouseEvent | PointerEvent | TouchEvent) => void);
|
||||||
|
private _highlight: Highlight | undefined;
|
||||||
|
|
||||||
constructor(stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) {
|
constructor(stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine}[]) {
|
||||||
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
||||||
|
|
@ -856,6 +858,21 @@ export class InjectedScript {
|
||||||
return error;
|
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() {
|
private _setupGlobalListenersRemovalDetection() {
|
||||||
const customEventName = '__playwright_global_listeners_check__';
|
const customEventName = '__playwright_global_listeners_check__';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,36 +50,42 @@ export class SdkObject extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Instrumentation {
|
export interface Instrumentation {
|
||||||
addListener(listener: InstrumentationListener): void;
|
addListener(listener: InstrumentationListener, context: BrowserContext | null): void;
|
||||||
removeListener(listener: InstrumentationListener): void;
|
removeListener(listener: InstrumentationListener): void;
|
||||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): 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>;
|
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||||
|
onPageOpen(page: Page): void;
|
||||||
|
onPageClose(page: Page): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstrumentationListener {
|
export interface InstrumentationListener {
|
||||||
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): 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>;
|
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||||
|
onPageOpen?(page: Page): void;
|
||||||
|
onPageClose?(page: Page): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInstrumentation(): Instrumentation {
|
export function createInstrumentation(): Instrumentation {
|
||||||
const listeners: InstrumentationListener[] = [];
|
const listeners = new Map<InstrumentationListener, BrowserContext | null>();
|
||||||
return new Proxy({}, {
|
return new Proxy({}, {
|
||||||
get: (obj: any, prop: string) => {
|
get: (obj: any, prop: string) => {
|
||||||
if (prop === 'addListener')
|
if (prop === 'addListener')
|
||||||
return (listener: InstrumentationListener) => listeners.push(listener);
|
return (listener: InstrumentationListener, context: BrowserContext | null) => listeners.set(listener, context);
|
||||||
if (prop === 'removeListener')
|
if (prop === 'removeListener')
|
||||||
return (listener: InstrumentationListener) => listeners.splice(listeners.indexOf(listener), 1);
|
return (listener: InstrumentationListener) => listeners.delete(listener);
|
||||||
if (!prop.startsWith('on'))
|
if (!prop.startsWith('on'))
|
||||||
return obj[prop];
|
return obj[prop];
|
||||||
return async (...params: any[]) => {
|
return async (sdkObject: SdkObject, ...params: any[]) => {
|
||||||
for (const listener of listeners)
|
for (const [listener, context] of listeners) {
|
||||||
await (listener as any)[prop]?.(...params);
|
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.pdf = delegate.pdf.bind(delegate);
|
||||||
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
||||||
this.selectors = browserContext.selectors();
|
this.selectors = browserContext.selectors();
|
||||||
|
this.instrumentation.onPageOpen(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initOpener(opener: PageDelegate | null) {
|
async initOpener(opener: PageDelegate | null) {
|
||||||
|
|
@ -208,6 +209,7 @@ export class Page extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
_didClose() {
|
_didClose() {
|
||||||
|
this.instrumentation.onPageClose(this);
|
||||||
this._frameManager.dispose();
|
this._frameManager.dispose();
|
||||||
this._frameThrottler.setEnabled(false);
|
this._frameThrottler.setEnabled(false);
|
||||||
assert(this._closedState !== 'closed', 'Page closed twice');
|
assert(this._closedState !== 'closed', 'Page closed twice');
|
||||||
|
|
@ -217,6 +219,7 @@ export class Page extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
_didCrash() {
|
_didCrash() {
|
||||||
|
this.instrumentation.onPageClose(this);
|
||||||
this._frameManager.dispose();
|
this._frameManager.dispose();
|
||||||
this._frameThrottler.setEnabled(false);
|
this._frameThrottler.setEnabled(false);
|
||||||
this.emit(Page.Events.Crash);
|
this.emit(Page.Events.Crash);
|
||||||
|
|
@ -224,6 +227,7 @@ export class Page extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
_didDisconnect() {
|
_didDisconnect() {
|
||||||
|
this.instrumentation.onPageClose(this);
|
||||||
this._frameManager.dispose();
|
this._frameManager.dispose();
|
||||||
this._frameThrottler.setEnabled(false);
|
this._frameThrottler.setEnabled(false);
|
||||||
assert(!this._disconnected, 'Page disconnected twice');
|
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;
|
const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.context()._options.strictSelectors;
|
||||||
return this.selectors.parseSelector(selector, strict);
|
return this.selectors.parseSelector(selector, strict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hideHighlight() {
|
||||||
|
await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {})));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Worker extends SdkObject {
|
export class Worker extends SdkObject {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { Selectors } from './selectors';
|
||||||
import { WebKit } from './webkit/webkit';
|
import { WebKit } from './webkit/webkit';
|
||||||
import { CallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
import { CallMetadata, createInstrumentation, SdkObject } from './instrumentation';
|
||||||
import { debugLogger } from '../utils/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
|
import { Page } from './page';
|
||||||
|
|
||||||
export class Playwright extends SdkObject {
|
export class Playwright extends SdkObject {
|
||||||
readonly selectors: Selectors;
|
readonly selectors: Selectors;
|
||||||
|
|
@ -33,14 +34,17 @@ export class Playwright extends SdkObject {
|
||||||
readonly firefox: Firefox;
|
readonly firefox: Firefox;
|
||||||
readonly webkit: WebKit;
|
readonly webkit: WebKit;
|
||||||
readonly options: PlaywrightOptions;
|
readonly options: PlaywrightOptions;
|
||||||
|
private _allPages = new Set<Page>();
|
||||||
|
|
||||||
constructor(sdkLanguage: string, isInternal: boolean) {
|
constructor(sdkLanguage: string, isInternal: boolean) {
|
||||||
super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
|
super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
|
||||||
this.instrumentation.addListener({
|
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);
|
debugLogger.log(logName as any, message);
|
||||||
}
|
}
|
||||||
});
|
}, null);
|
||||||
this.options = {
|
this.options = {
|
||||||
rootSdkObject: this,
|
rootSdkObject: this,
|
||||||
selectors: new Selectors(),
|
selectors: new Selectors(),
|
||||||
|
|
@ -53,6 +57,10 @@ export class Playwright extends SdkObject {
|
||||||
this.android = new Android(new AdbBackend(), this.options);
|
this.android = new Android(new AdbBackend(), this.options);
|
||||||
this.selectors = this.options.selectors;
|
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) {
|
export function createPlaywright(sdkLanguage: string, isInternal: boolean = false) {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export class ProgressController {
|
||||||
if (this._state === 'running')
|
if (this._state === 'running')
|
||||||
this.metadata.log.push(message);
|
this.metadata.log.push(message);
|
||||||
// Note: we might be sending logs after progress has finished, for example browser logs.
|
// 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)
|
if ('intermediateResult' in entry)
|
||||||
this._lastIntermediateResult = entry.intermediateResult;
|
this._lastIntermediateResult = entry.intermediateResult;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
|
||||||
await this.pause(sdkObject, metadata);
|
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);
|
debugLogger.log(logName as any, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import type InjectedScript from '../../injected/injectedScript';
|
||||||
import { generateSelector, querySelector } from '../../injected/selectorGenerator';
|
import { generateSelector, querySelector } from '../../injected/selectorGenerator';
|
||||||
import type { Point } from '../../../common/types';
|
import type { Point } from '../../../common/types';
|
||||||
import type { UIState } from '../recorder/recorderTypes';
|
import type { UIState } from '../recorder/recorderTypes';
|
||||||
|
import { Highlight } from '../../injected/highlight';
|
||||||
|
|
||||||
|
|
||||||
declare module globalThis {
|
declare module globalThis {
|
||||||
|
|
@ -32,11 +33,6 @@ declare module globalThis {
|
||||||
export class Recorder {
|
export class Recorder {
|
||||||
private _injectedScript: InjectedScript;
|
private _injectedScript: InjectedScript;
|
||||||
private _performingAction = false;
|
private _performingAction = false;
|
||||||
private _outerGlassPaneElement: HTMLElement;
|
|
||||||
private _glassPaneShadow: ShadowRoot;
|
|
||||||
private _innerGlassPaneElement: HTMLElement;
|
|
||||||
private _highlightElements: HTMLElement[] = [];
|
|
||||||
private _tooltipElement: HTMLElement;
|
|
||||||
private _listeners: (() => void)[] = [];
|
private _listeners: (() => void)[] = [];
|
||||||
private _hoveredModel: HighlightModel | null = null;
|
private _hoveredModel: HighlightModel | null = null;
|
||||||
private _hoveredElement: HTMLElement | null = null;
|
private _hoveredElement: HTMLElement | null = null;
|
||||||
|
|
@ -44,76 +40,15 @@ export class Recorder {
|
||||||
private _expectProgrammaticKeyUp = false;
|
private _expectProgrammaticKeyUp = false;
|
||||||
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||||
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
||||||
private _actionPointElement: HTMLElement;
|
|
||||||
private _actionPoint: Point | undefined;
|
private _actionPoint: Point | undefined;
|
||||||
private _actionSelector: string | undefined;
|
private _actionSelector: string | undefined;
|
||||||
private _params: { isUnderTest: boolean; };
|
private _params: { isUnderTest: boolean; };
|
||||||
|
private _highlight: Highlight;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) {
|
constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
this._outerGlassPaneElement = document.createElement('x-pw-glass');
|
this._highlight = new Highlight(params.isUnderTest);
|
||||||
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._refreshListenersIfNeeded();
|
this._refreshListenersIfNeeded();
|
||||||
injectedScript.onGlobalListenersRemoved.add(() => this._refreshListenersIfNeeded());
|
injectedScript.onGlobalListenersRemoved.add(() => this._refreshListenersIfNeeded());
|
||||||
|
|
@ -128,7 +63,7 @@ export class Recorder {
|
||||||
|
|
||||||
private _refreshListenersIfNeeded() {
|
private _refreshListenersIfNeeded() {
|
||||||
// Ensure we are attached to the current document, and we are on top (last element);
|
// 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;
|
return;
|
||||||
removeEventListeners(this._listeners);
|
removeEventListeners(this._listeners);
|
||||||
this._listeners = [
|
this._listeners = [
|
||||||
|
|
@ -144,11 +79,11 @@ export class Recorder {
|
||||||
addEventListener(document, 'focus', () => this._onFocus(), true),
|
addEventListener(document, 'focus', () => this._onFocus(), true),
|
||||||
addEventListener(document, 'scroll', () => {
|
addEventListener(document, 'scroll', () => {
|
||||||
this._hoveredModel = null;
|
this._hoveredModel = null;
|
||||||
this._actionPointElement.hidden = true;
|
this._highlight.hideActionPoint();
|
||||||
this._updateHighlight();
|
this._updateHighlight();
|
||||||
}, true),
|
}, true),
|
||||||
];
|
];
|
||||||
document.documentElement.appendChild(this._outerGlassPaneElement);
|
this._highlight.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _pollRecorderMode() {
|
private async _pollRecorderMode() {
|
||||||
|
|
@ -171,13 +106,10 @@ export class Recorder {
|
||||||
} else if (!actionPoint && !this._actionPoint) {
|
} else if (!actionPoint && !this._actionPoint) {
|
||||||
// All good.
|
// All good.
|
||||||
} else {
|
} else {
|
||||||
if (actionPoint) {
|
if (actionPoint)
|
||||||
this._actionPointElement.style.top = actionPoint.y + 'px';
|
this._highlight.showActionPoint(actionPoint.x, actionPoint.y);
|
||||||
this._actionPointElement.style.left = actionPoint.x + 'px';
|
else
|
||||||
this._actionPointElement.hidden = false;
|
this._highlight.hideActionPoint();
|
||||||
} else {
|
|
||||||
this._actionPointElement.hidden = true;
|
|
||||||
}
|
|
||||||
this._actionPoint = actionPoint;
|
this._actionPoint = actionPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,75 +261,8 @@ export class Recorder {
|
||||||
|
|
||||||
private _updateHighlight() {
|
private _updateHighlight() {
|
||||||
const elements = this._hoveredModel ? this._hoveredModel.elements : [];
|
const elements = this._hoveredModel ? this._hoveredModel.elements : [];
|
||||||
|
const selector = this._hoveredModel ? this._hoveredModel.selector : '';
|
||||||
// Code below should trigger one layout and leave with the
|
this._highlight.updateHighlight(elements, selector, this._mode === 'recording');
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onInput(event: Event) {
|
private _onInput(event: Event) {
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export class RecorderSupplement implements InstrumentationListener {
|
||||||
this._contextRecorder = new ContextRecorder(context, params);
|
this._contextRecorder = new ContextRecorder(context, params);
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._debugger = Debugger.lookup(context)!;
|
this._debugger = Debugger.lookup(context)!;
|
||||||
context.instrumentation.addListener(this);
|
context.instrumentation.addListener(this, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
async install() {
|
||||||
|
|
@ -248,7 +248,7 @@ export class RecorderSupplement implements InstrumentationListener {
|
||||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
|
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]);
|
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');
|
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)
|
if (state.options.screenshots)
|
||||||
this._startScreencast();
|
this._startScreencast();
|
||||||
if (state.options.snapshots)
|
if (state.options.snapshots)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue