feat(trace): highlight action target (#5776)
This commit is contained in:
parent
42e9a4703c
commit
fea6669473
|
|
@ -144,9 +144,10 @@ commandWithOpenOptions('pdf <url> <filename>', 'save page as pdf',
|
|||
if (process.env.PWTRACE) {
|
||||
program
|
||||
.command('show-trace [trace]')
|
||||
.option('--resources <dir>', 'load resources from shared folder')
|
||||
.description('Show trace viewer')
|
||||
.action(function(trace, command) {
|
||||
showTraceViewer(trace);
|
||||
showTraceViewer(trace, command.resources);
|
||||
}).on('--help', function() {
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
|
|
|
|||
|
|
@ -41,6 +41,16 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
return remoteObject.objectId!;
|
||||
}
|
||||
|
||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
||||
this._client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: func.toString(),
|
||||
arguments: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }),
|
||||
returnByValue: true,
|
||||
executionContextId: this._contextId,
|
||||
userGesture: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: expression,
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||
progress.log(` performing ${actionName} action`);
|
||||
progress.metadata.point = point;
|
||||
await progress.beforeInputAction();
|
||||
await progress.beforeInputAction(this);
|
||||
await action(point);
|
||||
progress.log(` ${actionName} action done`);
|
||||
progress.log(' waiting for scheduled navigations to finish');
|
||||
|
|
@ -458,7 +458,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
progress.log(' selecting specified option(s)');
|
||||
await progress.beforeInputAction();
|
||||
await progress.beforeInputAction(this);
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, optionsToSelect]) => {
|
||||
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], injected.selectOptions.bind(injected, optionsToSelect));
|
||||
}, optionsToSelect);
|
||||
|
|
@ -490,7 +490,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (filled === 'error:notconnected')
|
||||
return filled;
|
||||
progress.log(' element is visible, enabled and editable');
|
||||
await progress.beforeInputAction();
|
||||
await progress.beforeInputAction(this);
|
||||
if (filled === 'needsinput') {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
if (value)
|
||||
|
|
@ -537,7 +537,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await progress.beforeInputAction();
|
||||
await progress.beforeInputAction(this);
|
||||
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, files);
|
||||
});
|
||||
await this._page._doSlowMo();
|
||||
|
|
@ -574,7 +574,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (result !== 'done')
|
||||
return result;
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await progress.beforeInputAction();
|
||||
await progress.beforeInputAction(this);
|
||||
await this._page.keyboard.type(text, options);
|
||||
return 'done';
|
||||
}, 'input');
|
||||
|
|
@ -595,7 +595,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (result !== 'done')
|
||||
return result;
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
await progress.beforeInputAction();
|
||||
await progress.beforeInputAction(this);
|
||||
await this._page.keyboard.press(key, options);
|
||||
return 'done';
|
||||
}, 'input');
|
||||
|
|
|
|||
|
|
@ -40,6 +40,15 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
|||
return payload.result!.objectId!;
|
||||
}
|
||||
|
||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
||||
this._session.send('Runtime.callFunction', {
|
||||
functionDeclaration: func.toString(),
|
||||
args: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }) as any,
|
||||
returnByValue: true,
|
||||
executionContextId: this._executionContextId
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||
const payload = await this._session.send('Runtime.callFunction', {
|
||||
functionDeclaration: expression,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { Point, StackFrame } from '../common/types';
|
|||
import type { Browser } from './browser';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { BrowserType } from './browserType';
|
||||
import { ElementHandle } from './dom';
|
||||
import type { Frame } from './frames';
|
||||
import type { Page } from './page';
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ export interface Instrumentation {
|
|||
onContextDidDestroy(context: BrowserContext): Promise<void>;
|
||||
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||
onAfterInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
|
|
@ -78,7 +79,7 @@ export interface InstrumentationListener {
|
|||
onContextDidDestroy?(context: BrowserContext): Promise<void>;
|
||||
|
||||
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
||||
onAfterInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
onCallLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
|
|||
|
||||
export interface ExecutionContextDelegate {
|
||||
rawEvaluate(expression: string): Promise<ObjectId>;
|
||||
rawCallFunctionNoReply(func: Function, ...args: any[]): void;
|
||||
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
|
||||
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
||||
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
|
||||
|
|
@ -109,6 +110,10 @@ export class JSHandle<T = any> extends SdkObject {
|
|||
this._preview = 'JSHandle@' + String(this._objectId ? this._objectType : this._value);
|
||||
}
|
||||
|
||||
callFunctionNoReply(func: Function, arg: any) {
|
||||
this._context._delegate.rawCallFunctionNoReply(func, this, arg);
|
||||
}
|
||||
|
||||
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
|
||||
async evaluate<R>(pageFunction: FuncOn<T, void, R>, arg?: any): Promise<R>;
|
||||
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R> {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { TimeoutError } from '../utils/errors';
|
|||
import { assert, monotonicTime } from '../utils/utils';
|
||||
import { LogName } from '../utils/debugLogger';
|
||||
import { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||
import { ElementHandle } from './dom';
|
||||
|
||||
export interface Progress {
|
||||
log(message: string): void;
|
||||
|
|
@ -25,7 +26,7 @@ export interface Progress {
|
|||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
throwIfAborted(): void;
|
||||
beforeInputAction(): Promise<void>;
|
||||
beforeInputAction(element: ElementHandle): Promise<void>;
|
||||
afterInputAction(): Promise<void>;
|
||||
metadata: CallMetadata;
|
||||
}
|
||||
|
|
@ -87,8 +88,8 @@ export class ProgressController {
|
|||
if (this._state === 'aborted')
|
||||
throw new AbortedError();
|
||||
},
|
||||
beforeInputAction: async () => {
|
||||
await this.instrumentation.onBeforeInputAction(this.sdkObject, this.metadata);
|
||||
beforeInputAction: async (element: ElementHandle) => {
|
||||
await this.instrumentation.onBeforeInputAction(this.sdkObject, this.metadata, element);
|
||||
},
|
||||
afterInputAction: async () => {
|
||||
await this.instrumentation.onAfterInputAction(this.sdkObject, this.metadata);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { SnapshotRenderer } from './snapshotRenderer';
|
|||
import { SnapshotServer } from './snapshotServer';
|
||||
import { BaseSnapshotStorage } from './snapshotStorage';
|
||||
import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
||||
import { ElementHandle } from '../dom';
|
||||
|
||||
const kSnapshotInterval = 25;
|
||||
|
||||
|
|
@ -52,11 +53,11 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
|||
await this._server.stop();
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, snapshotName: string): Promise<SnapshotRenderer> {
|
||||
async captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle): Promise<SnapshotRenderer> {
|
||||
if (this._frameSnapshots.has(snapshotName))
|
||||
throw new Error('Duplicate snapshot name: ' + snapshotName);
|
||||
|
||||
this._snapshotter.captureSnapshot(page, snapshotName);
|
||||
this._snapshotter.captureSnapshot(page, snapshotName, element);
|
||||
return new Promise<SnapshotRenderer>(fulfill => {
|
||||
const listener = helper.addEventListener(this, 'snapshot', (renderer: SnapshotRenderer) => {
|
||||
if (renderer.snapshotName === snapshotName) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { BrowserContext } from '../browserContext';
|
|||
import { Page } from '../page';
|
||||
import { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
|
||||
import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
||||
import { ElementHandle } from '../dom';
|
||||
|
||||
|
||||
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
||||
|
|
@ -47,6 +48,8 @@ export class PersistentSnapshotter extends EventEmitter implements SnapshotterDe
|
|||
|
||||
async start(): Promise<void> {
|
||||
await fsMkdirAsync(this._resourcesDir, {recursive: true}).catch(() => {});
|
||||
await fsAppendFileAsync(this._networkTrace, Buffer.from([]));
|
||||
await fsAppendFileAsync(this._snapshotTrace, Buffer.from([]));
|
||||
await this._snapshotter.initialize();
|
||||
await this._snapshotter.setAutoSnapshotInterval(kSnapshotInterval);
|
||||
}
|
||||
|
|
@ -56,13 +59,13 @@ export class PersistentSnapshotter extends EventEmitter implements SnapshotterDe
|
|||
await this._writeArtifactChain;
|
||||
}
|
||||
|
||||
captureSnapshot(page: Page, snapshotName: string) {
|
||||
this._snapshotter.captureSnapshot(page, snapshotName);
|
||||
captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle) {
|
||||
this._snapshotter.captureSnapshot(page, snapshotName, element);
|
||||
}
|
||||
|
||||
onBlob(blob: SnapshotterBlob): void {
|
||||
this._writeArtifactChain = this._writeArtifactChain.then(async () => {
|
||||
await fsWriteFileAsync(path.join(this._resourcesDir, blob.sha1), blob.buffer);
|
||||
await fsWriteFileAsync(path.join(this._resourcesDir, blob.sha1), blob.buffer).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,9 +71,15 @@ export class SnapshotRenderer {
|
|||
|
||||
const snapshot = this._snapshots[this._index];
|
||||
let html = visit(snapshot.html, this._index);
|
||||
if (!html)
|
||||
return { html: '', resources: {} };
|
||||
|
||||
if (snapshot.doctype)
|
||||
html = `<!DOCTYPE ${snapshot.doctype}>` + html;
|
||||
html += `<script>${snapshotScript()}</script>`;
|
||||
html += `
|
||||
<style>*[__playwright_target__="${this.snapshotName}"] { background-color: #6fa8dc7f; }</style>
|
||||
<script>${snapshotScript()}</script>
|
||||
`;
|
||||
|
||||
const resources: { [key: string]: { resourceId: string, sha1?: string } } = {};
|
||||
for (const [url, contextResources] of this._contextResources) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import querystring from 'querystring';
|
|||
import { HttpServer } from '../../utils/httpServer';
|
||||
import type { RenderedFrameSnapshot } from './snapshotTypes';
|
||||
import { SnapshotStorage } from './snapshotStorage';
|
||||
import type { Point } from '../../common/types';
|
||||
|
||||
export class SnapshotServer {
|
||||
private _snapshotStorage: SnapshotStorage;
|
||||
|
|
@ -51,35 +52,7 @@ export class SnapshotServer {
|
|||
</style>
|
||||
<body>
|
||||
<script>
|
||||
if (navigator.serviceWorker) {
|
||||
navigator.serviceWorker.register('./service-worker.js');
|
||||
let showPromise = Promise.resolve();
|
||||
if (!navigator.serviceWorker.controller)
|
||||
showPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
|
||||
|
||||
let current = document.createElement('iframe');
|
||||
document.body.appendChild(current);
|
||||
let next = document.createElement('iframe');
|
||||
document.body.appendChild(next);
|
||||
next.style.visibility = 'hidden';
|
||||
const onload = () => {
|
||||
let temp = current;
|
||||
current = next;
|
||||
next = temp;
|
||||
current.style.visibility = 'visible';
|
||||
next.style.visibility = 'hidden';
|
||||
};
|
||||
current.onload = onload;
|
||||
next.onload = onload;
|
||||
|
||||
window.showSnapshot = async url => {
|
||||
await showPromise;
|
||||
next.src = url;
|
||||
};
|
||||
window.addEventListener('message', event => {
|
||||
window.showSnapshot(window.location.href + event.data.snapshotUrl);
|
||||
}, false);
|
||||
}
|
||||
(${rootScript})();
|
||||
</script>
|
||||
</body>
|
||||
`);
|
||||
|
|
@ -245,3 +218,59 @@ export class SnapshotServer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
showSnapshot: (url: string, point?: Point) => Promise<void>;
|
||||
}
|
||||
}
|
||||
function rootScript() {
|
||||
if (!navigator.serviceWorker)
|
||||
return;
|
||||
navigator.serviceWorker.register('./service-worker.js');
|
||||
let showPromise = Promise.resolve();
|
||||
if (!navigator.serviceWorker.controller) {
|
||||
showPromise = new Promise(resolve => {
|
||||
navigator.serviceWorker.oncontrollerchange = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
const pointElement = document.createElement('div');
|
||||
pointElement.style.position = 'fixed';
|
||||
pointElement.style.backgroundColor = 'red';
|
||||
pointElement.style.width = '20px';
|
||||
pointElement.style.height = '20px';
|
||||
pointElement.style.borderRadius = '10px';
|
||||
pointElement.style.margin = '-10px 0 0 -10px';
|
||||
pointElement.style.zIndex = '2147483647';
|
||||
|
||||
let current = document.createElement('iframe');
|
||||
document.body.appendChild(current);
|
||||
let next = document.createElement('iframe');
|
||||
document.body.appendChild(next);
|
||||
next.style.visibility = 'hidden';
|
||||
const onload = () => {
|
||||
const temp = current;
|
||||
current = next;
|
||||
next = temp;
|
||||
current.style.visibility = 'visible';
|
||||
next.style.visibility = 'hidden';
|
||||
};
|
||||
current.onload = onload;
|
||||
next.onload = onload;
|
||||
|
||||
(window as any).showSnapshot = async (url: string, options: { point?: Point } = {}) => {
|
||||
await showPromise;
|
||||
next.src = url;
|
||||
if (options.point) {
|
||||
pointElement.style.left = options.point.x + 'px';
|
||||
pointElement.style.top = options.point.y + 'px';
|
||||
document.documentElement.appendChild(pointElement);
|
||||
} else {
|
||||
pointElement.remove();
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', event => {
|
||||
window.showSnapshot(window.location.href + event.data.snapshotUrl);
|
||||
}, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { Frame } from '../frames';
|
|||
import { SnapshotData, frameSnapshotStreamer, kSnapshotBinding, kSnapshotStreamer } from './snapshotterInjected';
|
||||
import { calculateSha1, createGuid, monotonicTime } from '../../utils/utils';
|
||||
import { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
|
||||
import { ElementHandle } from '../dom';
|
||||
|
||||
export type SnapshotterBlob = {
|
||||
buffer: Buffer,
|
||||
|
|
@ -92,9 +93,12 @@ export class Snapshotter {
|
|||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
captureSnapshot(page: Page, snapshotName?: string) {
|
||||
captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle) {
|
||||
// This needs to be sync, as in not awaiting for anything before we issue the command.
|
||||
const expression = `window[${JSON.stringify(kSnapshotStreamer)}].captureSnapshot(${JSON.stringify(snapshotName)})`;
|
||||
element?.callFunctionNoReply((element: Element, snapshotName: string) => {
|
||||
element.setAttribute('__playwright_target__', snapshotName);
|
||||
}, snapshotName);
|
||||
const snapshotFrame = (frame: Frame) => {
|
||||
const context = frame._existingMainContext();
|
||||
context?.rawEvaluate(expression).catch(debugExceptionHandler);
|
||||
|
|
|
|||
|
|
@ -317,8 +317,6 @@ export function frameSnapshotStreamer() {
|
|||
|
||||
if (nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
// if (node === target)
|
||||
// attrs[' __playwright_target__] = '';
|
||||
if (nodeName === 'INPUT') {
|
||||
const value = (element as HTMLInputElement).value;
|
||||
expectValue('value');
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ declare global {
|
|||
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
||||
_playwrightRecorderCommitAction: () => Promise<void>;
|
||||
_playwrightRecorderState: () => Promise<UIState>;
|
||||
_playwrightResume: () => Promise<void>;
|
||||
_playwrightRecorderSetSelector: (selector: string) => Promise<void>;
|
||||
_playwrightRefreshOverlay: () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { StackFrame } from '../../../common/types';
|
||||
import { CallMetadata } from '../../instrumentation';
|
||||
|
||||
export type ContextCreatedTraceEvent = {
|
||||
timestamp: number,
|
||||
|
|
@ -47,27 +47,11 @@ export type PageDestroyedTraceEvent = {
|
|||
pageId: string,
|
||||
};
|
||||
|
||||
export type PageVideoTraceEvent = {
|
||||
timestamp: number,
|
||||
type: 'page-video',
|
||||
contextId: string,
|
||||
pageId: string,
|
||||
fileName: string,
|
||||
};
|
||||
|
||||
export type ActionTraceEvent = {
|
||||
timestamp: number,
|
||||
type: 'action',
|
||||
contextId: string,
|
||||
objectType: string,
|
||||
method: string,
|
||||
params: any,
|
||||
stack?: StackFrame[],
|
||||
pageId?: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
logs?: string[],
|
||||
error?: string,
|
||||
metadata: CallMetadata,
|
||||
snapshots?: { title: string, snapshotName: string }[],
|
||||
};
|
||||
|
||||
|
|
@ -109,7 +93,6 @@ export type TraceEvent =
|
|||
ContextDestroyedTraceEvent |
|
||||
PageCreatedTraceEvent |
|
||||
PageDestroyedTraceEvent |
|
||||
PageVideoTraceEvent |
|
||||
ActionTraceEvent |
|
||||
DialogOpenedEvent |
|
||||
DialogClosedEvent |
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import * as util from 'util';
|
||||
import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
|
||||
import { BrowserContext, Video } from '../../browserContext';
|
||||
import { BrowserContext } from '../../browserContext';
|
||||
import { Dialog } from '../../dialog';
|
||||
import { ElementHandle } from '../../dom';
|
||||
import { Frame, NavigationEvent } from '../../frames';
|
||||
import { helper, RegisteredListener } from '../../helper';
|
||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
|
||||
|
|
@ -28,18 +29,18 @@ import { PersistentSnapshotter } from '../../snapshot/persistentSnapshotter';
|
|||
import * as trace from '../common/traceEvents';
|
||||
|
||||
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
|
||||
const envTrace = getFromENV('PW_TRACE_DIR');
|
||||
const envTrace = getFromENV('PWTRACE_RESOURCE_DIR');
|
||||
|
||||
export class Tracer implements InstrumentationListener {
|
||||
private _contextTracers = new Map<BrowserContext, ContextTracer>();
|
||||
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
const traceDir = envTrace || context._options._traceDir;
|
||||
const traceDir = context._options._traceDir;
|
||||
if (!traceDir)
|
||||
return;
|
||||
const traceStorageDir = path.join(traceDir, 'resources');
|
||||
const resourcesDir = envTrace || path.join(traceDir, 'resources');
|
||||
const tracePath = path.join(traceDir, createGuid());
|
||||
const contextTracer = new ContextTracer(context, traceStorageDir, tracePath);
|
||||
const contextTracer = new ContextTracer(context, resourcesDir, tracePath);
|
||||
await contextTracer.start();
|
||||
this._contextTracers.set(context, contextTracer);
|
||||
}
|
||||
|
|
@ -52,15 +53,16 @@ export class Tracer implements InstrumentationListener {
|
|||
}
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint('before', sdkObject, metadata);
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?._captureSnapshot('action', sdkObject, metadata, element);
|
||||
}
|
||||
|
||||
async onAfterInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint('after', sdkObject, metadata);
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?._captureSnapshot('before', sdkObject, metadata, element);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?._captureSnapshot('after', sdkObject, metadata);
|
||||
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterCall(sdkObject, metadata);
|
||||
}
|
||||
}
|
||||
|
|
@ -80,12 +82,10 @@ class ContextTracer {
|
|||
private _snapshotter: PersistentSnapshotter;
|
||||
private _eventListeners: RegisteredListener[];
|
||||
private _disposed = false;
|
||||
private _traceFile: string;
|
||||
|
||||
constructor(context: BrowserContext, traceStorageDir: string, tracePrefix: string) {
|
||||
constructor(context: BrowserContext, resourcesDir: string, tracePrefix: string) {
|
||||
const traceFile = tracePrefix + '-actions.trace';
|
||||
this._contextId = 'context@' + createGuid();
|
||||
this._traceFile = traceFile;
|
||||
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
|
||||
const event: trace.ContextCreatedTraceEvent = {
|
||||
timestamp: monotonicTime(),
|
||||
|
|
@ -98,7 +98,7 @@ class ContextTracer {
|
|||
debugName: context._options._debugName,
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
this._snapshotter = new PersistentSnapshotter(context, tracePrefix, traceStorageDir);
|
||||
this._snapshotter = new PersistentSnapshotter(context, tracePrefix, resourcesDir);
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(context, BrowserContext.Events.Page, this._onPage.bind(this)),
|
||||
];
|
||||
|
|
@ -108,12 +108,12 @@ class ContextTracer {
|
|||
await this._snapshotter.start();
|
||||
}
|
||||
|
||||
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
async _captureSnapshot(name: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
||||
if (!sdkObject.attribution.page)
|
||||
return;
|
||||
const snapshotName = `${name}@${metadata.id}`;
|
||||
snapshotsForMetadata(metadata).push({ title: name, snapshotName });
|
||||
this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName);
|
||||
this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName, element);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
|
|
@ -123,16 +123,7 @@ class ContextTracer {
|
|||
timestamp: monotonicTime(),
|
||||
type: 'action',
|
||||
contextId: this._contextId,
|
||||
pageId: sdkObject.attribution.page.uniqueId,
|
||||
objectType: metadata.type,
|
||||
method: metadata.method,
|
||||
// FIXME: filter out evaluation snippets, binary
|
||||
params: metadata.params,
|
||||
stack: metadata.stack,
|
||||
startTime: metadata.startTime,
|
||||
endTime: metadata.endTime,
|
||||
logs: metadata.log.slice(),
|
||||
error: metadata.error,
|
||||
metadata,
|
||||
snapshots: snapshotsForMetadata(metadata),
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
|
|
@ -149,19 +140,6 @@ class ContextTracer {
|
|||
};
|
||||
this._appendTraceEvent(event);
|
||||
|
||||
page.on(Page.Events.VideoStarted, (video: Video) => {
|
||||
if (this._disposed)
|
||||
return;
|
||||
const event: trace.PageVideoTraceEvent = {
|
||||
timestamp: monotonicTime(),
|
||||
type: 'page-video',
|
||||
contextId: this._contextId,
|
||||
pageId,
|
||||
fileName: path.relative(path.dirname(this._traceFile), video._path),
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
});
|
||||
|
||||
page.on(Page.Events.Dialog, (dialog: Dialog) => {
|
||||
if (this._disposed)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class TraceModel {
|
|||
actions.reverse();
|
||||
|
||||
for (const action of actions) {
|
||||
while (resources.length && resources[0].timestamp > action.action.timestamp)
|
||||
while (resources.length && resources[0].timestamp > action.timestamp)
|
||||
action.resources.push(resources.shift()!);
|
||||
action.resources.reverse();
|
||||
}
|
||||
|
|
@ -79,14 +79,15 @@ export class TraceModel {
|
|||
break;
|
||||
}
|
||||
case 'action': {
|
||||
if (!kInterestingActions.includes(event.method))
|
||||
const metadata = event.metadata;
|
||||
if (metadata.method === 'waitForEventInfo')
|
||||
break;
|
||||
const { pageEntry } = this.pageEntries.get(event.pageId!)!;
|
||||
const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length;
|
||||
const { pageEntry } = this.pageEntries.get(metadata.pageId!)!;
|
||||
const actionId = event.contextId + '/' + metadata.pageId + '/' + pageEntry.actions.length;
|
||||
const action: ActionEntry = {
|
||||
actionId,
|
||||
action: event,
|
||||
resources: []
|
||||
resources: [],
|
||||
...event,
|
||||
};
|
||||
pageEntry.actions.push(action);
|
||||
break;
|
||||
|
|
@ -146,10 +147,7 @@ export type PageEntry = {
|
|||
interestingEvents: InterestingPageEvent[];
|
||||
}
|
||||
|
||||
export type ActionEntry = {
|
||||
export type ActionEntry = trace.ActionTraceEvent & {
|
||||
actionId: string;
|
||||
action: trace.ActionTraceEvent;
|
||||
resources: ResourceSnapshot[]
|
||||
};
|
||||
|
||||
const kInterestingActions = ['click', 'dblclick', 'hover', 'check', 'uncheck', 'tap', 'fill', 'press', 'type', 'selectOption', 'setInputFiles', 'goto', 'setContent', 'goBack', 'goForward', 'reload'];
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@
|
|||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as playwright from '../../../..';
|
||||
import { createPlaywright } from '../../playwright';
|
||||
import * as util from 'util';
|
||||
import { TraceModel } from './traceModel';
|
||||
import { TraceEvent } from '../common/traceEvents';
|
||||
import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
|
||||
import { SnapshotServer } from '../../snapshot/snapshotServer';
|
||||
import { PersistentSnapshotStorage } from '../../snapshot/snapshotStorage';
|
||||
import * as consoleApiSource from '../../../generated/consoleApiSource';
|
||||
import { isUnderTest } from '../../../utils/utils';
|
||||
import { internalCallMetadata } from '../../instrumentation';
|
||||
import { ProgressController } from '../../progress';
|
||||
|
||||
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
||||
|
||||
|
|
@ -34,8 +38,9 @@ type TraceViewerDocument = {
|
|||
class TraceViewer {
|
||||
private _document: TraceViewerDocument | undefined;
|
||||
|
||||
async show(traceDir: string) {
|
||||
const resourcesDir = path.join(traceDir, 'resources');
|
||||
async show(traceDir: string, resourcesDir?: string) {
|
||||
if (!resourcesDir)
|
||||
resourcesDir = path.join(traceDir, 'resources');
|
||||
const model = new TraceModel();
|
||||
this._document = {
|
||||
model,
|
||||
|
|
@ -56,7 +61,6 @@ class TraceViewer {
|
|||
// - "/snapshot/pageId/..." - actual snapshot html.
|
||||
// - "/snapshot/service-worker.js" - service worker that intercepts snapshot resources
|
||||
// and translates them into "/resources/<resourceId>".
|
||||
|
||||
const actionsTrace = fs.readdirSync(traceDir).find(name => name.endsWith('-actions.trace'))!;
|
||||
const tracePrefix = path.join(traceDir, actionsTrace.substring(0, actionsTrace.indexOf('-actions.trace')));
|
||||
const server = new HttpServer();
|
||||
|
|
@ -108,14 +112,33 @@ class TraceViewer {
|
|||
|
||||
const urlPrefix = await server.start();
|
||||
|
||||
const browser = await playwright.chromium.launch({ headless: false });
|
||||
const uiPage = await browser.newPage({ viewport: null });
|
||||
uiPage.on('close', () => process.exit(0));
|
||||
await uiPage.goto(urlPrefix + '/traceviewer/traceViewer/index.html');
|
||||
const traceViewerPlaywright = createPlaywright(true);
|
||||
const args = [
|
||||
'--app=data:text/html,',
|
||||
'--window-position=1280,10',
|
||||
];
|
||||
if (isUnderTest())
|
||||
args.push(`--remote-debugging-port=0`);
|
||||
const context = await traceViewerPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', {
|
||||
// TODO: store language in the trace.
|
||||
sdkLanguage: 'javascript',
|
||||
args,
|
||||
noDefaultViewport: true,
|
||||
headless: !!process.env.PWCLI_HEADLESS_FOR_TEST,
|
||||
useWebSocket: isUnderTest()
|
||||
});
|
||||
const controller = new ProgressController(internalCallMetadata(), context._browser);
|
||||
await controller.run(async progress => {
|
||||
await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
|
||||
});
|
||||
await context.extendInjectedScript(consoleApiSource.source);
|
||||
const [page] = context.pages();
|
||||
page.on('close', () => process.exit(0));
|
||||
await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/traceviewer/traceViewer/index.html');
|
||||
}
|
||||
}
|
||||
|
||||
export async function showTraceViewer(traceDir: string) {
|
||||
export async function showTraceViewer(traceDir: string, resourcesDir?: string) {
|
||||
const traceViewer = new TraceViewer();
|
||||
await traceViewer.show(traceDir);
|
||||
await traceViewer.show(traceDir, resourcesDir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,16 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
||||
this._session.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: func.toString(),
|
||||
objectId: args.find(a => a instanceof js.JSHandle)!._objectId,
|
||||
arguments: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }),
|
||||
returnByValue: true,
|
||||
emulateUserGesture: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||
try {
|
||||
let response = await this._session.send('Runtime.callFunctionOn', {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const ActionList: React.FC<ActionListProps> = ({
|
|||
}) => {
|
||||
const targetAction = highlightedAction || selectedAction;
|
||||
return <div className='action-list'>{actions.map(actionEntry => {
|
||||
const { action, actionId } = actionEntry;
|
||||
const { metadata, actionId } = actionEntry;
|
||||
return <div
|
||||
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
|
||||
key={actionId}
|
||||
|
|
@ -44,10 +44,10 @@ export const ActionList: React.FC<ActionListProps> = ({
|
|||
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
||||
>
|
||||
<div className='action-header'>
|
||||
<div className={'action-error codicon codicon-issues'} hidden={!actionEntry.action.error} />
|
||||
<div className='action-title'>{action.method}</div>
|
||||
{action.params.selector && <div className='action-selector' title={action.params.selector}>{action.params.selector}</div>}
|
||||
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
||||
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
||||
<div className='action-title'>{metadata.method}</div>
|
||||
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
||||
</div>
|
||||
</div>;
|
||||
})}</div>;
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ export const LogsTab: React.FunctionComponent<{
|
|||
}> = ({ actionEntry }) => {
|
||||
let logs: string[] = [];
|
||||
if (actionEntry) {
|
||||
logs = actionEntry.action.logs || [];
|
||||
if (actionEntry.action.error)
|
||||
logs = [actionEntry.action.error, ...logs];
|
||||
logs = actionEntry.metadata.log || [];
|
||||
if (actionEntry.metadata.error)
|
||||
logs = [actionEntry.metadata.error, ...logs];
|
||||
}
|
||||
return <div className='logs-tab'>{
|
||||
logs.map((logLine, index) => {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import * as React from 'react';
|
|||
import { Expandable } from './helpers';
|
||||
import type { ResourceSnapshot } from '../../../server/snapshot/snapshotTypes';
|
||||
|
||||
const utf8Encoder = new TextDecoder('utf-8');
|
||||
|
||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot,
|
||||
index: number,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import './snapshotTab.css';
|
|||
import * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import { msToString } from '../../uiUtils';
|
||||
import type { Point } from '../../../common/types';
|
||||
|
||||
export const SnapshotTab: React.FunctionComponent<{
|
||||
actionEntry: ActionEntry | undefined,
|
||||
|
|
@ -30,7 +31,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||
|
||||
const snapshots = actionEntry ? (actionEntry.action.snapshots || []) : [];
|
||||
const snapshots = actionEntry ? (actionEntry.snapshots || []) : [];
|
||||
const { pageId, time } = selection || { pageId: undefined, time: 0 };
|
||||
|
||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||
|
|
@ -39,16 +40,20 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
return;
|
||||
|
||||
let snapshotUri = undefined;
|
||||
let point: Point | undefined = undefined;
|
||||
if (pageId) {
|
||||
snapshotUri = `${pageId}?time=${time}`;
|
||||
} else if (actionEntry) {
|
||||
const snapshot = snapshots[snapshotIndex];
|
||||
if (snapshot && snapshot.snapshotName)
|
||||
snapshotUri = `${actionEntry.action.pageId}?name=${snapshot.snapshotName}`;
|
||||
if (snapshot && snapshot.snapshotName) {
|
||||
snapshotUri = `${actionEntry.metadata.pageId}?name=${snapshot.snapshotName}`;
|
||||
if (snapshot.snapshotName.includes('action'))
|
||||
point = actionEntry.metadata.point;
|
||||
}
|
||||
}
|
||||
const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,Snapshot is not available';
|
||||
try {
|
||||
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl);
|
||||
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
|
||||
} catch (e) {
|
||||
}
|
||||
}, [actionEntry, snapshotIndex, pageId, time]);
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ export const SourceTab: React.FunctionComponent<{
|
|||
const stackInfo = React.useMemo<StackInfo>(() => {
|
||||
if (!actionEntry)
|
||||
return '';
|
||||
const { action } = actionEntry;
|
||||
if (!action.stack)
|
||||
const { metadata } = actionEntry;
|
||||
if (!metadata.stack)
|
||||
return '';
|
||||
const frames = action.stack;
|
||||
const frames = metadata.stack;
|
||||
return {
|
||||
frames,
|
||||
fileContent: new Map(),
|
||||
|
|
|
|||
|
|
@ -54,17 +54,17 @@ export const Timeline: React.FunctionComponent<{
|
|||
const bars: TimelineBar[] = [];
|
||||
for (const page of context.pages) {
|
||||
for (const entry of page.actions) {
|
||||
let detail = entry.action.params.selector || '';
|
||||
if (entry.action.method === 'goto')
|
||||
detail = entry.action.params.url || '';
|
||||
let detail = entry.metadata.params.selector || '';
|
||||
if (entry.metadata.method === 'goto')
|
||||
detail = entry.metadata.params.url || '';
|
||||
bars.push({
|
||||
entry,
|
||||
leftTime: entry.action.startTime,
|
||||
rightTime: entry.action.endTime,
|
||||
leftPosition: timeToPosition(measure.width, boundaries, entry.action.startTime),
|
||||
rightPosition: timeToPosition(measure.width, boundaries, entry.action.endTime),
|
||||
label: entry.action.method + ' ' + detail,
|
||||
type: entry.action.method,
|
||||
leftTime: entry.metadata.startTime,
|
||||
rightTime: entry.metadata.endTime,
|
||||
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
|
||||
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
|
||||
label: entry.metadata.method + ' ' + detail,
|
||||
type: entry.metadata.method,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ActionEntry, ContextEntry, TraceModel } from '../../../server/trace/viewer/traceModel';
|
||||
import { ActionEntry, ContextEntry } from '../../../server/trace/viewer/traceModel';
|
||||
import { ActionList } from './actionList';
|
||||
import { TabbedPane } from './tabbedPane';
|
||||
import { Timeline } from './timeline';
|
||||
|
|
|
|||
|
|
@ -144,13 +144,10 @@ fixtures.isLinux.init(async ({ platform }, run) => {
|
|||
}, { scope: 'worker' });
|
||||
|
||||
fixtures.contextOptions.init(async ({ video, testInfo }, run) => {
|
||||
if (video) {
|
||||
await run({
|
||||
recordVideo: { dir: testInfo.outputPath('') },
|
||||
});
|
||||
} else {
|
||||
await run({});
|
||||
}
|
||||
await run({
|
||||
recordVideo: video ? { dir: testInfo.outputPath('') } : undefined,
|
||||
_traceDir: process.env.PWTRACE ? testInfo.outputPath('') : undefined,
|
||||
} as any);
|
||||
});
|
||||
|
||||
fixtures.contextFactory.init(async ({ browser, contextOptions, testInfo, screenshotOnFailure }, run) => {
|
||||
|
|
|
|||
|
|
@ -175,12 +175,46 @@ describe('snapshots', (suite, { mode }) => {
|
|||
const button = await previewPage.frames()[3].waitForSelector('button');
|
||||
expect(await button.textContent()).toBe('Hello iframe');
|
||||
});
|
||||
|
||||
it('should capture snapshot target', async ({ snapshotter, page, toImpl }) => {
|
||||
await page.setContent('<button>Hello</button><button>World</button>');
|
||||
{
|
||||
const handle = await page.$('text=Hello');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot', toImpl(handle));
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON __playwright_target__=\"snapshot\">Hello</BUTTON><BUTTON>World</BUTTON>');
|
||||
}
|
||||
{
|
||||
const handle = await page.$('text=World');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2', toImpl(handle));
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON __playwright_target__=\"snapshot\">Hello</BUTTON><BUTTON __playwright_target__=\"snapshot2\">World</BUTTON>');
|
||||
}
|
||||
});
|
||||
|
||||
it('should collect on attribute change', async ({ snapshotter, page, toImpl }) => {
|
||||
await page.setContent('<button>Hello</button>');
|
||||
{
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON>Hello</BUTTON>');
|
||||
}
|
||||
const handle = await page.$('text=Hello')!;
|
||||
await handle.evaluate(element => element.setAttribute('data', 'one'));
|
||||
{
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON data="one">Hello</BUTTON>');
|
||||
}
|
||||
await handle.evaluate(element => element.setAttribute('data', 'two'));
|
||||
{
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON data="two">Hello</BUTTON>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function distillSnapshot(snapshot) {
|
||||
const { html } = snapshot.render();
|
||||
return html
|
||||
.replace(/<script>[.\s\S]+<\/script>/, '')
|
||||
.replace(/<style>.*__playwright_target__.*<\/style>/, '')
|
||||
.replace(/<BASE href="about:blank">/, '')
|
||||
.replace(/<BASE href="http:\/\/localhost:[\d]+\/empty.html">/, '')
|
||||
.replace(/<HTML>/, '')
|
||||
|
|
@ -188,5 +222,5 @@ function distillSnapshot(snapshot) {
|
|||
.replace(/<HEAD>/, '')
|
||||
.replace(/<\/HEAD>/, '')
|
||||
.replace(/<BODY>/, '')
|
||||
.replace(/<\/BODY>/, '');
|
||||
.replace(/<\/BODY>/, '').trim();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue