mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 05:46:28 +03:00
chore: split trace events into phases (#21696)
This commit is contained in:
parent
40a6eff8f2
commit
c45d8749b0
@ -259,7 +259,6 @@ export class DispatcherConnection {
|
||||
method,
|
||||
params: params || {},
|
||||
log: [],
|
||||
snapshots: []
|
||||
};
|
||||
|
||||
if (sdkObject && params?.info?.waitId) {
|
||||
|
@ -1391,7 +1391,7 @@ export class Frame extends SdkObject {
|
||||
const injected = await context.injectedScript();
|
||||
progress.throwIfAborted();
|
||||
|
||||
const { log, matches, received, missingRecevied } = await injected.evaluate(async (injected, { info, options, snapshotName }) => {
|
||||
const { log, matches, received, missingRecevied } = await injected.evaluate(async (injected, { info, options, callId }) => {
|
||||
const elements = info ? injected.querySelectorAll(info.parsed, document) : [];
|
||||
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
||||
let log = '';
|
||||
@ -1401,10 +1401,10 @@ export class Frame extends SdkObject {
|
||||
throw injected.strictModeViolationError(info!.parsed, elements);
|
||||
else if (elements.length)
|
||||
log = ` locator resolved to ${injected.previewNode(elements[0])}`;
|
||||
if (snapshotName)
|
||||
injected.markTargetElements(new Set(elements), snapshotName);
|
||||
if (callId)
|
||||
injected.markTargetElements(new Set(elements), callId);
|
||||
return { log, ...(await injected.expect(elements[0], options, elements)) };
|
||||
}, { info, options, snapshotName: progress.metadata.afterSnapshot });
|
||||
}, { info, options, callId: metadata.id });
|
||||
|
||||
if (log)
|
||||
progress.log(log);
|
||||
@ -1552,16 +1552,16 @@ export class Frame extends SdkObject {
|
||||
progress.throwIfAborted();
|
||||
if (!resolved)
|
||||
return continuePolling;
|
||||
const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, snapshotName }) => {
|
||||
const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, callId }) => {
|
||||
const callback = injected.eval(callbackText) as ElementCallback<T, R>;
|
||||
const element = injected.querySelector(info.parsed, document, info.strict);
|
||||
if (!element)
|
||||
return { success: false };
|
||||
const log = ` locator resolved to ${injected.previewNode(element)}`;
|
||||
if (snapshotName)
|
||||
injected.markTargetElements(new Set([element]), snapshotName);
|
||||
if (callId)
|
||||
injected.markTargetElements(new Set([element]), callId);
|
||||
return { log, success: true, value: callback(injected, element, taskData as T) };
|
||||
}, { info: resolved.info, callbackText, taskData, snapshotName: progress.metadata.afterSnapshot });
|
||||
}, { info: resolved.info, callbackText, taskData, callId: progress.metadata.id });
|
||||
|
||||
if (log)
|
||||
progress.log(log);
|
||||
|
@ -1087,14 +1087,14 @@ export class InjectedScript {
|
||||
}
|
||||
}
|
||||
|
||||
markTargetElements(markedElements: Set<Element>, snapshotName: string) {
|
||||
markTargetElements(markedElements: Set<Element>, callId: string) {
|
||||
for (const e of this._markedTargetElements) {
|
||||
if (!markedElements.has(e))
|
||||
e.removeAttribute('__playwright_target__');
|
||||
}
|
||||
for (const e of markedElements) {
|
||||
if (!this._markedTargetElements.has(e))
|
||||
e.setAttribute('__playwright_target__', snapshotName);
|
||||
e.setAttribute('__playwright_target__', callId);
|
||||
}
|
||||
this._markedTargetElements = markedElements;
|
||||
}
|
||||
|
@ -112,7 +112,6 @@ export function serverSideCallMetadata(): CallMetadata {
|
||||
method: '',
|
||||
params: {},
|
||||
log: [],
|
||||
snapshots: [],
|
||||
isServerSide: true,
|
||||
};
|
||||
}
|
||||
|
@ -575,7 +575,6 @@ class ContextRecorder extends EventEmitter {
|
||||
method: action,
|
||||
params,
|
||||
log: [],
|
||||
snapshots: [],
|
||||
};
|
||||
this._generator.willPerformAction(actionInContext);
|
||||
|
||||
|
@ -104,14 +104,14 @@ export class Snapshotter {
|
||||
eventsHelper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle): Promise<void> {
|
||||
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<void> {
|
||||
// Prepare expression synchronously.
|
||||
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`;
|
||||
|
||||
// In a best-effort manner, without waiting for it, mark target element.
|
||||
element?.callFunctionNoReply((element: Element, snapshotName: string) => {
|
||||
element.setAttribute('__playwright_target__', snapshotName);
|
||||
}, snapshotName);
|
||||
element?.callFunctionNoReply((element: Element, callId: string) => {
|
||||
element.setAttribute('__playwright_target__', callId);
|
||||
}, callId);
|
||||
|
||||
// In each frame, in a non-stalling manner, capture the snapshots.
|
||||
const snapshots = page.frames().map(async frame => {
|
||||
@ -121,6 +121,7 @@ export class Snapshotter {
|
||||
return;
|
||||
|
||||
const snapshot: FrameSnapshot = {
|
||||
callId,
|
||||
snapshotName,
|
||||
pageId: page.guid,
|
||||
frameId: frame.guid,
|
||||
|
@ -71,7 +71,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
private _snapshotter?: Snapshotter;
|
||||
private _harTracer: HarTracer;
|
||||
private _screencastListeners: RegisteredListener[] = [];
|
||||
private _pendingCalls = new Map<string, { sdkObject: SdkObject, metadata: CallMetadata, beforeSnapshot: Promise<void>, actionSnapshot?: Promise<void>, afterSnapshot?: Promise<void> }>();
|
||||
private _context: BrowserContext | APIRequestContext;
|
||||
private _state: RecordingState | undefined;
|
||||
private _isStopping = false;
|
||||
@ -249,19 +248,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
if (this._state?.options.screenshots)
|
||||
this._stopScreencast();
|
||||
|
||||
for (const { sdkObject, metadata, beforeSnapshot, actionSnapshot, afterSnapshot } of this._pendingCalls.values()) {
|
||||
await Promise.all([beforeSnapshot, actionSnapshot, afterSnapshot]);
|
||||
let callMetadata = metadata;
|
||||
if (!afterSnapshot) {
|
||||
// Note: we should not modify metadata here to avoid side-effects in any other place.
|
||||
callMetadata = {
|
||||
...metadata,
|
||||
error: { error: { name: 'Error', message: 'Action was interrupted' } },
|
||||
};
|
||||
}
|
||||
await this.onAfterCall(sdkObject, callMetadata);
|
||||
}
|
||||
|
||||
if (state.options.snapshots)
|
||||
await this._snapshotter?.stop();
|
||||
|
||||
@ -309,7 +295,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
return result;
|
||||
}
|
||||
|
||||
async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
|
||||
async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
||||
if (!this._snapshotter)
|
||||
return;
|
||||
if (!sdkObject.attribution.page)
|
||||
@ -318,47 +304,43 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
return;
|
||||
if (!shouldCaptureSnapshot(metadata))
|
||||
return;
|
||||
const snapshotName = `${name}@${metadata.id}`;
|
||||
metadata.snapshots.push({ title: name, snapshotName });
|
||||
// We have |element| for input actions (page.click and handle.click)
|
||||
// and |sdkObject| element for accessors like handle.textContent.
|
||||
if (!element && sdkObject instanceof ElementHandle)
|
||||
element = sdkObject;
|
||||
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName, element).catch(() => {});
|
||||
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName, element).catch(() => {});
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
||||
const event = createBeforeActionTraceEvent(metadata);
|
||||
if (!event)
|
||||
return Promise.resolve();
|
||||
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||
// Set afterSnapshot name for all the actions that operate selectors.
|
||||
// Elements resolved from selectors will be marked on the snapshot.
|
||||
metadata.afterSnapshot = `after@${metadata.id}`;
|
||||
const beforeSnapshot = this._captureSnapshot('before', sdkObject, metadata);
|
||||
this._pendingCalls.set(metadata.id, { sdkObject, metadata, beforeSnapshot });
|
||||
await beforeSnapshot;
|
||||
event.beforeSnapshot = `before@${metadata.id}`;
|
||||
this._appendTraceEvent(event);
|
||||
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
||||
const event = createInputActionTraceEvent(metadata);
|
||||
if (!event)
|
||||
return Promise.resolve();
|
||||
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||
const actionSnapshot = this._captureSnapshot('action', sdkObject, metadata, element);
|
||||
this._pendingCalls.get(metadata.id)!.actionSnapshot = actionSnapshot;
|
||||
await actionSnapshot;
|
||||
event.inputSnapshot = `input@${metadata.id}`;
|
||||
this._appendTraceEvent(event);
|
||||
return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata, element);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
const event = createAfterActionTraceEvent(metadata);
|
||||
if (!event)
|
||||
return Promise.resolve();
|
||||
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||
const pendingCall = this._pendingCalls.get(metadata.id);
|
||||
if (!pendingCall || pendingCall.afterSnapshot)
|
||||
return;
|
||||
if (!sdkObject.attribution.context) {
|
||||
this._pendingCalls.delete(metadata.id);
|
||||
return;
|
||||
}
|
||||
pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata);
|
||||
await pendingCall.afterSnapshot;
|
||||
const event = createActionTraceEvent(metadata);
|
||||
if (event)
|
||||
this._appendTraceEvent(event);
|
||||
this._pendingCalls.delete(metadata.id);
|
||||
event.afterSnapshot = `after@${metadata.id}`;
|
||||
this._appendTraceEvent(event);
|
||||
return this._captureSnapshot(event.afterSnapshot, sdkObject, metadata);
|
||||
}
|
||||
|
||||
onEvent(sdkObject: SdkObject, event: trace.EventTraceEvent) {
|
||||
@ -492,24 +474,41 @@ export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
|
||||
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
|
||||
}
|
||||
|
||||
function createActionTraceEvent(metadata: CallMetadata): trace.ActionTraceEvent | null {
|
||||
function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActionTraceEvent | null {
|
||||
if (metadata.internal || metadata.method.startsWith('tracing'))
|
||||
return null;
|
||||
return {
|
||||
type: 'action',
|
||||
type: 'before',
|
||||
callId: metadata.id,
|
||||
startTime: metadata.startTime,
|
||||
endTime: metadata.endTime,
|
||||
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
|
||||
class: metadata.type,
|
||||
method: metadata.method,
|
||||
params: metadata.params,
|
||||
wallTime: metadata.wallTime || Date.now(),
|
||||
log: metadata.log,
|
||||
snapshots: metadata.snapshots,
|
||||
error: metadata.error?.error,
|
||||
result: metadata.result,
|
||||
point: metadata.point,
|
||||
pageId: metadata.pageId,
|
||||
};
|
||||
}
|
||||
|
||||
function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
|
||||
if (metadata.internal || metadata.method.startsWith('tracing'))
|
||||
return null;
|
||||
return {
|
||||
type: 'input',
|
||||
callId: metadata.id,
|
||||
point: metadata.point,
|
||||
};
|
||||
}
|
||||
|
||||
function createAfterActionTraceEvent(metadata: CallMetadata): trace.AfterActionTraceEvent | null {
|
||||
if (metadata.internal || metadata.method.startsWith('tracing'))
|
||||
return null;
|
||||
return {
|
||||
type: 'after',
|
||||
callId: metadata.id,
|
||||
endTime: metadata.endTime,
|
||||
log: metadata.log,
|
||||
error: metadata.error?.error,
|
||||
result: metadata.result,
|
||||
};
|
||||
}
|
||||
|
@ -56,11 +56,11 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
||||
this._harTracer.stop();
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle): Promise<SnapshotRenderer> {
|
||||
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<SnapshotRenderer> {
|
||||
if (this._frameSnapshots.has(snapshotName))
|
||||
throw new Error('Duplicate snapshot name: ' + snapshotName);
|
||||
|
||||
this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {});
|
||||
this._snapshotter.captureSnapshot(page, callId, snapshotName, element).catch(() => {});
|
||||
return new Promise<SnapshotRenderer>(fulfill => {
|
||||
const disposable = this.onSnapshotEvent((renderer: SnapshotRenderer) => {
|
||||
if (renderer.snapshotName === snapshotName) {
|
||||
|
@ -16,11 +16,11 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import type EventEmitter from 'events';
|
||||
import type { ClientSideCallMetadata, StackFrame } from '@protocol/channels';
|
||||
import type { ClientSideCallMetadata, SerializedError, StackFrame } from '@protocol/channels';
|
||||
import type { SerializedClientSideCallMetadata, SerializedStack, SerializedStackFrame } from './isomorphic/traceUtils';
|
||||
import { yazl, yauzl } from '../zipBundle';
|
||||
import { ManualPromise } from './manualPromise';
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import type { AfterActionTraceEvent, BeforeActionTraceEvent, TraceEvent } from '@trace/trace';
|
||||
import { calculateSha1 } from './crypto';
|
||||
import { monotonicTime } from './time';
|
||||
|
||||
@ -96,7 +96,7 @@ export async function mergeTraceFiles(fileName: string, temporaryTraceFiles: str
|
||||
await mergePromise;
|
||||
}
|
||||
|
||||
export async function saveTraceFile(fileName: string, traceEvents: ActionTraceEvent[], saveSources: boolean) {
|
||||
export async function saveTraceFile(fileName: string, traceEvents: TraceEvent[], saveSources: boolean) {
|
||||
const lines: string[] = traceEvents.map(e => JSON.stringify(e));
|
||||
const zipFile = new yazl.ZipFile();
|
||||
zipFile.addBuffer(Buffer.from(lines.join('\n')), 'trace.trace');
|
||||
@ -104,8 +104,10 @@ export async function saveTraceFile(fileName: string, traceEvents: ActionTraceEv
|
||||
if (saveSources) {
|
||||
const sourceFiles = new Set<string>();
|
||||
for (const event of traceEvents) {
|
||||
for (const frame of event.stack || [])
|
||||
sourceFiles.add(frame.file);
|
||||
if (event.type === 'before') {
|
||||
for (const frame of event.stack || [])
|
||||
sourceFiles.add(frame.file);
|
||||
}
|
||||
}
|
||||
for (const sourceFile of sourceFiles) {
|
||||
await fs.promises.readFile(sourceFile, 'utf8').then(source => {
|
||||
@ -120,23 +122,30 @@ export async function saveTraceFile(fileName: string, traceEvents: ActionTraceEv
|
||||
});
|
||||
}
|
||||
|
||||
export function createTraceEventForExpect(apiName: string, expected: any, stack: StackFrame[], wallTime: number): ActionTraceEvent {
|
||||
export function createBeforeActionTraceEventForExpect(callId: string, apiName: string, expected: any, stack: StackFrame[]): BeforeActionTraceEvent {
|
||||
return {
|
||||
type: 'action',
|
||||
callId: 'expect@' + wallTime,
|
||||
wallTime,
|
||||
type: 'before',
|
||||
callId,
|
||||
wallTime: Date.now(),
|
||||
startTime: monotonicTime(),
|
||||
endTime: 0,
|
||||
class: 'Test',
|
||||
method: 'step',
|
||||
apiName,
|
||||
params: { expected: generatePreview(expected) },
|
||||
snapshots: [],
|
||||
log: [],
|
||||
stack,
|
||||
};
|
||||
}
|
||||
|
||||
export function createAfterActionTraceEventForExpect(callId: string, error?: SerializedError['error']): AfterActionTraceEvent {
|
||||
return {
|
||||
type: 'after',
|
||||
callId,
|
||||
endTime: monotonicTime(),
|
||||
log: [],
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
function generatePreview(value: any, visited = new Set<any>()): string {
|
||||
if (visited.has(value))
|
||||
return '';
|
||||
|
@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { captureRawStack, createTraceEventForExpect, monotonicTime, pollAgainstTimeout } from 'playwright-core/lib/utils';
|
||||
import {
|
||||
captureRawStack,
|
||||
createAfterActionTraceEventForExpect,
|
||||
createBeforeActionTraceEventForExpect,
|
||||
pollAgainstTimeout } from 'playwright-core/lib/utils';
|
||||
import type { ExpectZone } from 'playwright-core/lib/utils';
|
||||
import {
|
||||
toBeChecked,
|
||||
@ -72,6 +76,8 @@ export type SyncExpectationResult = {
|
||||
// The replacement is compatible with pretty-format package.
|
||||
const printSubstring = (val: string): string => val.replace(/"|\\/g, '\\$&');
|
||||
|
||||
let lastCallId = 0;
|
||||
|
||||
export const printReceivedStringContainExpectedSubstring = (
|
||||
received: string,
|
||||
start: number,
|
||||
@ -215,9 +221,9 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
testInfo.currentStep = step;
|
||||
|
||||
const generateTraceEvent = matcherName !== 'poll' && matcherName !== 'toPass';
|
||||
const traceEvent = generateTraceEvent ? createTraceEventForExpect(defaultTitle, args[0], stackFrames, wallTime) : undefined;
|
||||
if (traceEvent)
|
||||
testInfo._traceEvents.push(traceEvent);
|
||||
const callId = ++lastCallId;
|
||||
if (generateTraceEvent)
|
||||
testInfo._traceEvents.push(createBeforeActionTraceEventForExpect(`expect@${callId}`, defaultTitle, args[0], stackFrames));
|
||||
|
||||
const reportStepError = (jestError: Error) => {
|
||||
const message = jestError.message;
|
||||
@ -243,11 +249,11 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
}
|
||||
|
||||
const serializerError = serializeError(jestError);
|
||||
if (traceEvent) {
|
||||
traceEvent.error = { name: jestError.name, message: jestError.message, stack: jestError.stack };
|
||||
traceEvent.endTime = monotonicTime();
|
||||
step.complete({ error: serializerError });
|
||||
if (generateTraceEvent) {
|
||||
const error = { name: jestError.name, message: jestError.message, stack: jestError.stack };
|
||||
testInfo._traceEvents.push(createAfterActionTraceEventForExpect(`expect@${callId}`, error));
|
||||
}
|
||||
step.complete({ error: serializerError });
|
||||
if (this._info.isSoft)
|
||||
testInfo._failWithError(serializerError, false /* isHardError */);
|
||||
else
|
||||
@ -255,8 +261,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
};
|
||||
|
||||
const finalizer = () => {
|
||||
if (traceEvent)
|
||||
traceEvent.endTime = monotonicTime();
|
||||
if (generateTraceEvent)
|
||||
testInfo._traceEvents.push(createAfterActionTraceEventForExpect(`expect@${callId}`));
|
||||
step.complete({});
|
||||
};
|
||||
|
||||
|
@ -36,8 +36,6 @@ export type CallMetadata = {
|
||||
wallTime?: number;
|
||||
location?: { file: string, line?: number, column?: number };
|
||||
log: string[];
|
||||
afterSnapshot?: string;
|
||||
snapshots: { title: string, snapshotName: string }[];
|
||||
error?: SerializedError;
|
||||
result?: any;
|
||||
point?: Point;
|
||||
|
@ -22,12 +22,14 @@ export class SnapshotRenderer {
|
||||
readonly snapshotName: string | undefined;
|
||||
_resources: ResourceSnapshot[];
|
||||
private _snapshot: FrameSnapshot;
|
||||
private _callId: string;
|
||||
|
||||
constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) {
|
||||
this._resources = resources;
|
||||
this._snapshots = snapshots;
|
||||
this._index = index;
|
||||
this._snapshot = snapshots[index];
|
||||
this._callId = snapshots[index].callId;
|
||||
this.snapshotName = snapshots[index].snapshotName;
|
||||
}
|
||||
|
||||
@ -102,7 +104,7 @@ export class SnapshotRenderer {
|
||||
const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : '';
|
||||
html = prefix + [
|
||||
'<style>*,*::before,*::after { visibility: hidden }</style>',
|
||||
`<style>*[__playwright_target__="${this.snapshotName}"] { background-color: #6fa8dc7f; }</style>`,
|
||||
`<style>*[__playwright_target__="${this._callId}"] { background-color: #6fa8dc7f; }</style>`,
|
||||
`<script>${snapshotScript()}</script>`
|
||||
].join('') + html;
|
||||
|
||||
|
@ -37,7 +37,8 @@ export class TraceModel {
|
||||
}
|
||||
|
||||
async load(traceURL: string, progress: (done: number, total: number) => void) {
|
||||
this._backend = traceURL.endsWith('json') ? new FetchTraceModelBackend(traceURL) : new ZipTraceModelBackend(traceURL, progress);
|
||||
const isLive = traceURL.endsWith('json');
|
||||
this._backend = isLive ? new FetchTraceModelBackend(traceURL) : new ZipTraceModelBackend(traceURL, progress);
|
||||
|
||||
const ordinals: string[] = [];
|
||||
let hasSource = false;
|
||||
@ -55,16 +56,25 @@ export class TraceModel {
|
||||
|
||||
for (const ordinal of ordinals) {
|
||||
const contextEntry = createEmptyContext();
|
||||
const actionMap = new Map<string, trace.ActionTraceEvent>();
|
||||
contextEntry.traceUrl = traceURL;
|
||||
contextEntry.hasSource = hasSource;
|
||||
|
||||
const trace = await this._backend.readText(ordinal + 'trace.trace') || '';
|
||||
for (const line of trace.split('\n'))
|
||||
this.appendEvent(contextEntry, line);
|
||||
this.appendEvent(contextEntry, actionMap, line);
|
||||
|
||||
const network = await this._backend.readText(ordinal + 'trace.network') || '';
|
||||
for (const line of network.split('\n'))
|
||||
this.appendEvent(contextEntry, line);
|
||||
this.appendEvent(contextEntry, actionMap, line);
|
||||
|
||||
contextEntry.actions = [...actionMap.values()].sort((a1, a2) => a1.startTime - a2.startTime);
|
||||
if (!isLive) {
|
||||
for (const action of contextEntry.actions) {
|
||||
if (!action.endTime && !action.error)
|
||||
action.error = { name: 'Error', message: 'Timed out' };
|
||||
}
|
||||
}
|
||||
|
||||
const stacks = await this._backend.readText(ordinal + 'trace.stacks');
|
||||
if (stacks) {
|
||||
@ -73,7 +83,6 @@ export class TraceModel {
|
||||
action.stack = action.stack || callMetadata.get(action.callId);
|
||||
}
|
||||
|
||||
contextEntry.actions.sort((a1, a2) => a1.startTime - a2.startTime);
|
||||
this.contextEntries.push(contextEntry);
|
||||
}
|
||||
}
|
||||
@ -102,7 +111,7 @@ export class TraceModel {
|
||||
return pageEntry;
|
||||
}
|
||||
|
||||
appendEvent(contextEntry: ContextEntry, line: string) {
|
||||
appendEvent(contextEntry: ContextEntry, actionMap: Map<string, trace.ActionTraceEvent>, line: string) {
|
||||
if (!line)
|
||||
return;
|
||||
const event = this._modernize(JSON.parse(line));
|
||||
@ -124,8 +133,27 @@ export class TraceModel {
|
||||
this._pageEntry(contextEntry, event.pageId).screencastFrames.push(event);
|
||||
break;
|
||||
}
|
||||
case 'before': {
|
||||
actionMap.set(event.callId, { ...event, type: 'action', endTime: 0, log: [] });
|
||||
break;
|
||||
}
|
||||
case 'input': {
|
||||
const existing = actionMap.get(event.callId);
|
||||
existing!.inputSnapshot = event.inputSnapshot;
|
||||
existing!.point = event.point;
|
||||
break;
|
||||
}
|
||||
case 'after': {
|
||||
const existing = actionMap.get(event.callId);
|
||||
existing!.afterSnapshot = event.afterSnapshot;
|
||||
existing!.endTime = event.endTime;
|
||||
existing!.log = event.log;
|
||||
existing!.result = event.result;
|
||||
existing!.error = event.error;
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
contextEntry!.actions.push(event);
|
||||
actionMap.set(event.callId, event);
|
||||
break;
|
||||
}
|
||||
case 'event': {
|
||||
@ -144,10 +172,10 @@ export class TraceModel {
|
||||
this._snapshotStorage!.addFrameSnapshot(event.snapshot);
|
||||
break;
|
||||
}
|
||||
if (event.type === 'action') {
|
||||
if (event.type === 'action' || event.type === 'before')
|
||||
contextEntry.startTime = Math.min(contextEntry.startTime, event.startTime);
|
||||
if (event.type === 'action' || event.type === 'after')
|
||||
contextEntry.endTime = Math.max(contextEntry.endTime, event.endTime);
|
||||
}
|
||||
if (event.type === 'event') {
|
||||
contextEntry.startTime = Math.min(contextEntry.startTime, event.time);
|
||||
contextEntry.endTime = Math.max(contextEntry.endTime, event.time);
|
||||
@ -251,7 +279,9 @@ export class TraceModel {
|
||||
params: metadata.params,
|
||||
wallTime: metadata.wallTime || Date.now(),
|
||||
log: metadata.log,
|
||||
snapshots: metadata.snapshots,
|
||||
beforeSnapshot: metadata.snapshots.find(s => s.snapshotName === 'before')?.snapshotName,
|
||||
inputSnapshot: metadata.snapshots.find(s => s.snapshotName === 'input')?.snapshotName,
|
||||
afterSnapshot: metadata.snapshots.find(s => s.snapshotName === 'after')?.snapshotName,
|
||||
error: metadata.error?.error,
|
||||
result: metadata.result,
|
||||
point: metadata.point,
|
||||
|
@ -42,11 +42,13 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
const [pickerVisible, setPickerVisible] = React.useState(false);
|
||||
|
||||
const { snapshots, snapshotInfoUrl, snapshotUrl, pointX, pointY, popoutUrl } = React.useMemo(() => {
|
||||
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
||||
for (const snapshot of action?.snapshots || [])
|
||||
snapshotMap.set(snapshot.title, snapshot);
|
||||
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
|
||||
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||
const actionSnapshot = action?.inputSnapshot || action?.afterSnapshot;
|
||||
const snapshots = [
|
||||
actionSnapshot ? { title: 'action', snapshotName: actionSnapshot } : undefined,
|
||||
action?.beforeSnapshot ? { title: 'before', snapshotName: action?.beforeSnapshot } : undefined,
|
||||
action?.afterSnapshot ? { title: 'after', snapshotName: action.afterSnapshot } : undefined,
|
||||
].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||
|
||||
let snapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';
|
||||
let popoutUrl: string | undefined;
|
||||
let snapshotInfoUrl: string | undefined;
|
||||
@ -60,7 +62,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
params.set('name', snapshot.snapshotName);
|
||||
snapshotUrl = new URL(`snapshot/${action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||
snapshotInfoUrl = new URL(`snapshotInfo/${action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||
if (snapshot.snapshotName.includes('action')) {
|
||||
if (snapshot.title === 'action') {
|
||||
pointX = action.point?.x;
|
||||
pointY = action.point?.y;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import type { XtermDataSource } from '@web/components/xtermWrapper';
|
||||
import { XtermWrapper } from '@web/components/xtermWrapper';
|
||||
import { Expandable } from '@web/components/expandable';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
import { artifactsFolderName } from '@testIsomorphic/folders';
|
||||
|
||||
let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {};
|
||||
let runWatchedTests = (fileName: string) => {};
|
||||
@ -392,30 +393,42 @@ const TraceView: React.FC<{
|
||||
result: TestResult | undefined,
|
||||
}> = ({ outputDir, testCase, result }) => {
|
||||
const [model, setModel] = React.useState<MultiTraceModel | undefined>();
|
||||
const [currentStep, setCurrentStep] = React.useState(0);
|
||||
const [counter, setCounter] = React.useState(0);
|
||||
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pollTimer.current)
|
||||
clearTimeout(pollTimer.current);
|
||||
|
||||
// Test finished.
|
||||
const isFinished = result && result.duration >= 0;
|
||||
if (isFinished) {
|
||||
const attachment = result.attachments.find(a => a.name === 'trace');
|
||||
if (attachment && attachment.path)
|
||||
loadSingleTraceFile(attachment.path).then(setModel);
|
||||
if (!result) {
|
||||
setModel(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const traceLocation = `${outputDir}/.playwright-artifacts-${result?.workerIndex}/traces/${testCase?.id}.json`;
|
||||
// Test finished.
|
||||
const attachment = result && result.duration >= 0 && result.attachments.find(a => a.name === 'trace');
|
||||
if (attachment && attachment.path) {
|
||||
loadSingleTraceFile(attachment.path).then(model => setModel(model));
|
||||
return;
|
||||
}
|
||||
|
||||
const traceLocation = `${outputDir}/${artifactsFolderName(result!.workerIndex)}/traces/${testCase?.id}.json`;
|
||||
// Start polling running test.
|
||||
pollTimer.current = setTimeout(() => {
|
||||
loadSingleTraceFile(traceLocation).then(setModel).then(() => {
|
||||
setCurrentStep(currentStep + 1);
|
||||
});
|
||||
pollTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const model = await loadSingleTraceFile(traceLocation);
|
||||
setModel(model);
|
||||
} catch {
|
||||
setModel(undefined);
|
||||
} finally {
|
||||
setCounter(counter + 1);
|
||||
}
|
||||
}, 250);
|
||||
}, [result, outputDir, testCase, currentStep, setCurrentStep]);
|
||||
return () => {
|
||||
if (pollTimer.current)
|
||||
clearTimeout(pollTimer.current);
|
||||
};
|
||||
}, [result, outputDir, testCase, setModel, counter, setCounter]);
|
||||
|
||||
return <Workbench key='workbench' model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
|
||||
};
|
||||
|
@ -97,6 +97,8 @@ export type ResourceOverride = {
|
||||
};
|
||||
|
||||
export type FrameSnapshot = {
|
||||
// There was no callId in the original, we are intentionally regressing it.
|
||||
callId: string;
|
||||
snapshotName?: string,
|
||||
pageId: string,
|
||||
frameId: string,
|
||||
|
@ -39,6 +39,7 @@ export type ResourceOverride = {
|
||||
|
||||
export type FrameSnapshot = {
|
||||
snapshotName?: string,
|
||||
callId: string,
|
||||
pageId: string,
|
||||
frameId: string,
|
||||
frameUrl: string,
|
||||
|
@ -51,23 +51,35 @@ export type ScreencastFrameTraceEvent = {
|
||||
timestamp: number,
|
||||
};
|
||||
|
||||
export type ActionTraceEvent = {
|
||||
type: 'action',
|
||||
export type BeforeActionTraceEvent = {
|
||||
type: 'before',
|
||||
callId: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
apiName: string;
|
||||
class: string;
|
||||
method: string;
|
||||
params: any;
|
||||
wallTime: number;
|
||||
log: string[];
|
||||
snapshots: { title: string, snapshotName: string }[];
|
||||
beforeSnapshot?: string;
|
||||
stack?: StackFrame[];
|
||||
pageId?: string;
|
||||
};
|
||||
|
||||
export type InputActionTraceEvent = {
|
||||
type: 'input',
|
||||
callId: string;
|
||||
inputSnapshot?: string;
|
||||
point?: Point;
|
||||
};
|
||||
|
||||
export type AfterActionTraceEvent = {
|
||||
type: 'after',
|
||||
callId: string;
|
||||
endTime: number;
|
||||
afterSnapshot?: string;
|
||||
log: string[];
|
||||
error?: SerializedError['error'];
|
||||
result?: any;
|
||||
point?: Point;
|
||||
pageId?: string;
|
||||
};
|
||||
|
||||
export type EventTraceEvent = {
|
||||
@ -96,10 +108,19 @@ export type FrameSnapshotTraceEvent = {
|
||||
snapshot: FrameSnapshot,
|
||||
};
|
||||
|
||||
export type ActionTraceEvent = {
|
||||
type: 'action',
|
||||
} & Omit<BeforeActionTraceEvent, 'type'>
|
||||
& Omit<AfterActionTraceEvent, 'type'>
|
||||
& Omit<InputActionTraceEvent, 'type'>;
|
||||
|
||||
export type TraceEvent =
|
||||
ContextCreatedTraceEvent |
|
||||
ScreencastFrameTraceEvent |
|
||||
ActionTraceEvent |
|
||||
BeforeActionTraceEvent |
|
||||
InputActionTraceEvent |
|
||||
AfterActionTraceEvent |
|
||||
EventTraceEvent |
|
||||
ObjectTraceEvent |
|
||||
ResourceSnapshotTraceEvent |
|
||||
|
@ -18,7 +18,7 @@ import type { Frame, Page } from 'playwright-core';
|
||||
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
|
||||
import type { StackFrame } from '../../packages/protocol/src/channels';
|
||||
import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils';
|
||||
import type { ActionTraceEvent } from '../../packages/trace/src/trace';
|
||||
import type { ActionTraceEvent, TraceEvent } from '../../packages/trace/src/trace';
|
||||
|
||||
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
||||
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
||||
@ -101,11 +101,36 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
|
||||
resources.set(entry, await zipFS.read(entry));
|
||||
zipFS.close();
|
||||
|
||||
const actionMap = new Map<string, ActionTraceEvent>();
|
||||
const events: any[] = [];
|
||||
for (const traceFile of [...resources.keys()].filter(name => name.endsWith('.trace'))) {
|
||||
for (const line of resources.get(traceFile)!.toString().split('\n')) {
|
||||
if (line)
|
||||
events.push(JSON.parse(line));
|
||||
if (line) {
|
||||
const event = JSON.parse(line) as TraceEvent;
|
||||
if (event.type === 'before') {
|
||||
const action: ActionTraceEvent = {
|
||||
...event,
|
||||
type: 'action',
|
||||
endTime: 0,
|
||||
log: []
|
||||
};
|
||||
events.push(action);
|
||||
actionMap.set(event.callId, action);
|
||||
} else if (event.type === 'input') {
|
||||
const existing = actionMap.get(event.callId);
|
||||
existing.inputSnapshot = event.inputSnapshot;
|
||||
existing.point = event.point;
|
||||
} else if (event.type === 'after') {
|
||||
const existing = actionMap.get(event.callId);
|
||||
existing.afterSnapshot = event.afterSnapshot;
|
||||
existing.endTime = event.endTime;
|
||||
existing.log = event.log;
|
||||
existing.error = event.error;
|
||||
existing.result = event.result;
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,19 +29,19 @@ const it = contextTest.extend<{ snapshotter: InMemorySnapshotter }>({
|
||||
it.describe('snapshots', () => {
|
||||
it('should collect snapshot', async ({ page, toImpl, snapshotter }) => {
|
||||
await page.setContent('<button>Hello</button>');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON>Hello</BUTTON>');
|
||||
});
|
||||
|
||||
it('should preserve BASE and other content on reset', async ({ page, toImpl, snapshotter, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
const html1 = snapshot1.render().html;
|
||||
expect(html1).toContain(`<BASE href="${server.EMPTY_PAGE}"`);
|
||||
await snapshotter.reset();
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
|
||||
const html2 = snapshot2.render().html;
|
||||
expect(html2.replace(`"snapshot2"`, `"snapshot1"`)).toEqual(html1);
|
||||
expect(html2.replace(`"call@2"`, `"call@1"`)).toEqual(html1);
|
||||
});
|
||||
|
||||
it('should capture resources', async ({ page, toImpl, server, snapshotter }) => {
|
||||
@ -50,7 +50,7 @@ it.describe('snapshots', () => {
|
||||
route.fulfill({ body: 'button { color: red; }', }).catch(() => {});
|
||||
});
|
||||
await page.setContent('<link rel="stylesheet" href="style.css"><button>Hello</button>');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
const resource = snapshot.resourceByUrl(`http://localhost:${server.PORT}/style.css`);
|
||||
expect(resource).toBeTruthy();
|
||||
});
|
||||
@ -59,36 +59,36 @@ it.describe('snapshots', () => {
|
||||
await page.setContent('<button>Hello</button>');
|
||||
const snapshots = [];
|
||||
snapshotter.onSnapshotEvent(snapshot => snapshots.push(snapshot));
|
||||
await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
|
||||
expect(snapshots.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should respect inline CSSOM change', async ({ page, toImpl, snapshotter }) => {
|
||||
await page.setContent('<style>button { color: red; }</style><button>Hello</button>');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot1)).toBe('<STYLE>button { color: red; }</STYLE><BUTTON>Hello</BUTTON>');
|
||||
|
||||
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
|
||||
expect(distillSnapshot(snapshot2)).toBe('<STYLE>button { color: blue; }</STYLE><BUTTON>Hello</BUTTON>');
|
||||
});
|
||||
|
||||
it('should respect node removal', async ({ page, toImpl, snapshotter }) => {
|
||||
await page.setContent('<div><button id="button1"></button><button id="button2"></button></div>');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot1)).toBe('<DIV><BUTTON id=\"button1\"></BUTTON><BUTTON id=\"button2\"></BUTTON></DIV>');
|
||||
await page.evaluate(() => document.getElementById('button2').remove());
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
|
||||
expect(distillSnapshot(snapshot2)).toBe('<DIV><BUTTON id=\"button1\"></BUTTON></DIV>');
|
||||
});
|
||||
|
||||
it('should respect attr removal', async ({ page, toImpl, snapshotter }) => {
|
||||
await page.setContent('<div id="div" attr1="1" attr2="2"></div>');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot1)).toBe('<DIV id=\"div\" attr1=\"1\" attr2=\"2\"></DIV>');
|
||||
await page.evaluate(() => document.getElementById('div').removeAttribute('attr2'));
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot@call@2');
|
||||
expect(distillSnapshot(snapshot2)).toBe('<DIV id=\"div\" attr1=\"1\"></DIV>');
|
||||
});
|
||||
|
||||
@ -96,21 +96,21 @@ it.describe('snapshots', () => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<!DOCTYPE foo><body>hi</body>');
|
||||
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot)).toBe('<!DOCTYPE foo>hi');
|
||||
});
|
||||
|
||||
it('should replace meta charset attr that specifies charset', async ({ page, server, toImpl, snapshotter }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<meta charset="shift-jis" />');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot)).toBe('<META charset="utf-8">');
|
||||
});
|
||||
|
||||
it('should replace meta content attr that specifies charset', async ({ page, server, toImpl, snapshotter }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot)).toBe('<META http-equiv="Content-Type" content="text/html; charset=utf-8">');
|
||||
});
|
||||
|
||||
@ -121,11 +121,11 @@ it.describe('snapshots', () => {
|
||||
});
|
||||
await page.setContent('<link rel="stylesheet" href="style.css"><button>Hello</button>');
|
||||
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot1)).toBe('<LINK rel=\"stylesheet\" href=\"style.css\"><BUTTON>Hello</BUTTON>');
|
||||
|
||||
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
const resource = snapshot2.resourceByUrl(`http://localhost:${server.PORT}/style.css`);
|
||||
expect((await snapshotter.resourceContentForTest(resource.response.content._sha1)).toString()).toBe('button { color: blue; }');
|
||||
});
|
||||
@ -146,7 +146,7 @@ it.describe('snapshots', () => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
||||
for (let counter = 0; ; ++counter) {
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot' + counter);
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@' + counter, 'snapshot@call@' + counter);
|
||||
const text = distillSnapshot(snapshot).replace(/frame@[^"]+["]/, '<id>"');
|
||||
if (text === '<FRAMESET><FRAME __playwright_src__=\"/snapshot/<id>\"></FRAME></FRAMESET>')
|
||||
break;
|
||||
@ -191,7 +191,7 @@ it.describe('snapshots', () => {
|
||||
|
||||
// Marking iframe hierarchy is racy, do not expect snapshot, wait for it.
|
||||
for (let counter = 0; ; ++counter) {
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot' + counter);
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@' + counter, 'snapshot@call@' + counter);
|
||||
const text = distillSnapshot(snapshot).replace(/frame@[^"]+["]/, '<id>"');
|
||||
if (text === '<IFRAME __playwright_src__=\"/snapshot/<id>\"></IFRAME>')
|
||||
break;
|
||||
@ -203,31 +203,31 @@ it.describe('snapshots', () => {
|
||||
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, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"snapshot\">Hello</BUTTON><BUTTON>World</BUTTON>');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1', toImpl(handle));
|
||||
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"call@1\">Hello</BUTTON><BUTTON>World</BUTTON>');
|
||||
}
|
||||
{
|
||||
const handle = await page.$('text=World');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2', toImpl(handle));
|
||||
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"snapshot\">Hello</BUTTON><BUTTON __playwright_target__=\"snapshot2\">World</BUTTON>');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2', toImpl(handle));
|
||||
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"call@1\">Hello</BUTTON><BUTTON __playwright_target__=\"call@2\">World</BUTTON>');
|
||||
}
|
||||
});
|
||||
|
||||
it('should collect on attribute change', async ({ page, toImpl, snapshotter }) => {
|
||||
await page.setContent('<button>Hello</button>');
|
||||
{
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
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');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
|
||||
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');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot@call@2');
|
||||
expect(distillSnapshot(snapshot)).toBe('<BUTTON data="two">Hello</BUTTON>');
|
||||
}
|
||||
});
|
||||
@ -251,11 +251,11 @@ it.describe('snapshots', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const renderer1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||
const renderer1 = await snapshotter.captureSnapshot(toImpl(page), 'call1', 'snapshot@call@1');
|
||||
// Expect some adopted style sheets.
|
||||
expect(distillSnapshot(renderer1)).toContain('__playwright_style_sheet_');
|
||||
|
||||
const renderer2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||
const renderer2 = await snapshotter.captureSnapshot(toImpl(page), 'call2', 'snapshot@call@2');
|
||||
const snapshot2 = renderer2.snapshot();
|
||||
// Second snapshot should be just a copy of the first one.
|
||||
expect(snapshot2.html).toEqual([[1, 13]]);
|
||||
@ -263,7 +263,7 @@ it.describe('snapshots', () => {
|
||||
|
||||
it('should not navigate on anchor clicks', async ({ page, toImpl, snapshotter }) => {
|
||||
await page.setContent('<a href="https://example.com">example.com</a>');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
|
||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
|
||||
expect(distillSnapshot(snapshot)).toBe('<A href="link://https://example.com">example.com</A>');
|
||||
});
|
||||
});
|
||||
|
@ -113,7 +113,8 @@ test('should not include buffers in the trace', async ({ context, page, server,
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
||||
const screenshotEvent = events.find(e => e.type === 'action' && e.apiName === 'page.screenshot');
|
||||
expect(screenshotEvent.snapshots.length).toBe(2);
|
||||
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
|
||||
expect(screenshotEvent.afterSnapshot).toBeTruthy();
|
||||
expect(screenshotEvent.result).toEqual({});
|
||||
});
|
||||
|
||||
@ -405,7 +406,6 @@ test('should include interrupted actions', async ({ context, page, server }, tes
|
||||
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
|
||||
const clickEvent = events.find(e => e.apiName === 'page.click');
|
||||
expect(clickEvent).toBeTruthy();
|
||||
expect(clickEvent.error.message).toBe('Action was interrupted');
|
||||
});
|
||||
|
||||
test('should throw when starting with different options', async ({ context }) => {
|
||||
@ -448,8 +448,6 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
|
||||
'page.click',
|
||||
'page.click',
|
||||
]);
|
||||
expect(trace1.events.find(e => e.apiName === 'page.click' && !!e.error)).toBeTruthy();
|
||||
expect(trace1.events.find(e => e.apiName === 'page.click' && e.error?.message === 'Action was interrupted')).toBeTruthy();
|
||||
expect(trace1.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
|
||||
expect(trace1.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
|
||||
|
||||
|
@ -115,15 +115,15 @@ it('should support has:locator', async ({ page, trace }) => {
|
||||
await expect(page.locator(`div`, {
|
||||
has: page.locator(`text=world`)
|
||||
})).toHaveCount(1);
|
||||
expect(await page.locator(`div`, {
|
||||
expect(removeHighlight(await page.locator(`div`, {
|
||||
has: page.locator(`text=world`)
|
||||
}).evaluate(e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
||||
}).evaluate(e => e.outerHTML))).toBe(`<div><span>world</span></div>`);
|
||||
await expect(page.locator(`div`, {
|
||||
has: page.locator(`text="hello"`)
|
||||
})).toHaveCount(1);
|
||||
expect(await page.locator(`div`, {
|
||||
expect(removeHighlight(await page.locator(`div`, {
|
||||
has: page.locator(`text="hello"`)
|
||||
}).evaluate(e => e.outerHTML)).toBe(`<div><span>hello</span></div>`);
|
||||
}).evaluate(e => e.outerHTML))).toBe(`<div><span>hello</span></div>`);
|
||||
await expect(page.locator(`div`, {
|
||||
has: page.locator(`xpath=./span`)
|
||||
})).toHaveCount(2);
|
||||
@ -133,9 +133,9 @@ it('should support has:locator', async ({ page, trace }) => {
|
||||
await expect(page.locator(`div`, {
|
||||
has: page.locator(`span`, { hasText: 'wor' })
|
||||
})).toHaveCount(1);
|
||||
expect(await page.locator(`div`, {
|
||||
expect(removeHighlight(await page.locator(`div`, {
|
||||
has: page.locator(`span`, { hasText: 'wor' })
|
||||
}).evaluate(e => e.outerHTML)).toBe(`<div><span>world</span></div>`);
|
||||
}).evaluate(e => e.outerHTML))).toBe(`<div><span>world</span></div>`);
|
||||
await expect(page.locator(`div`, {
|
||||
has: page.locator(`span`),
|
||||
hasText: 'wor',
|
||||
@ -180,3 +180,7 @@ it('alias methods coverage', async ({ page }) => {
|
||||
await expect(page.locator('div').getByRole('button')).toHaveCount(1);
|
||||
await expect(page.mainFrame().locator('button')).toHaveCount(1);
|
||||
});
|
||||
|
||||
function removeHighlight(markup: string) {
|
||||
return markup.replace(/\s__playwright_target__="[^"]+"/, '');
|
||||
}
|
@ -123,6 +123,7 @@ const testFiles = {
|
||||
};
|
||||
|
||||
test.slow(true, 'Multiple browser launches in each test');
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('should work with screenshot: on', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
|
Loading…
Reference in New Issue
Block a user