feat: include screencast in trace (#6128)
This commit is contained in:
parent
0c00891b80
commit
d0db4f6737
|
|
@ -157,7 +157,7 @@ export class CRPage implements PageDelegate {
|
||||||
for (const session of this._sessions.values())
|
for (const session of this._sessions.values())
|
||||||
session.dispose();
|
session.dispose();
|
||||||
this._page._didClose();
|
this._page._didClose();
|
||||||
this._mainFrameSession._stopScreencast().catch(() => {});
|
this._mainFrameSession._stopVideoRecording().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
||||||
|
|
@ -290,6 +290,19 @@ export class CRPage implements PageDelegate {
|
||||||
return this._sessionForHandle(handle)._scrollRectIntoViewIfNeeded(handle, rect);
|
return this._sessionForHandle(handle)._scrollRectIntoViewIfNeeded(handle, rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setScreencastEnabled(enabled: boolean): Promise<void> {
|
||||||
|
if (enabled) {
|
||||||
|
await this._mainFrameSession._startScreencast(this, {
|
||||||
|
format: 'jpeg',
|
||||||
|
quality: 90,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 600,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this._mainFrameSession._stopScreencast(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rafCountForStablePosition(): number {
|
rafCountForStablePosition(): number {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -357,6 +370,7 @@ class FrameSession {
|
||||||
private _swappedIn = false;
|
private _swappedIn = false;
|
||||||
private _videoRecorder: VideoRecorder | null = null;
|
private _videoRecorder: VideoRecorder | null = null;
|
||||||
private _screencastId: string | null = null;
|
private _screencastId: string | null = null;
|
||||||
|
private _screencastClients = new Set<any>();
|
||||||
|
|
||||||
constructor(crPage: CRPage, client: CRSession, targetId: string, parentSession: FrameSession | null) {
|
constructor(crPage: CRPage, client: CRSession, targetId: string, parentSession: FrameSession | null) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
|
|
@ -429,7 +443,7 @@ class FrameSession {
|
||||||
await this._crPage._browserContext._ensureVideosPath();
|
await this._crPage._browserContext._ensureVideosPath();
|
||||||
// Note: it is important to start video recorder before sending Page.startScreencast,
|
// Note: it is important to start video recorder before sending Page.startScreencast,
|
||||||
// and it is equally important to send Page.startScreencast before sending Runtime.runIfWaitingForDebugger.
|
// and it is equally important to send Page.startScreencast before sending Runtime.runIfWaitingForDebugger.
|
||||||
await this._startVideoRecorder(screencastId, screencastOptions);
|
await this._createVideoRecorder(screencastId, screencastOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lifecycleEventsEnabled: Promise<any>;
|
let lifecycleEventsEnabled: Promise<any>;
|
||||||
|
|
@ -511,7 +525,7 @@ class FrameSession {
|
||||||
for (const source of this._crPage._page._evaluateOnNewDocumentSources)
|
for (const source of this._crPage._page._evaluateOnNewDocumentSources)
|
||||||
promises.push(this._evaluateOnNewDocument(source, 'main'));
|
promises.push(this._evaluateOnNewDocument(source, 'main'));
|
||||||
if (screencastOptions)
|
if (screencastOptions)
|
||||||
promises.push(this._startScreencast(screencastOptions));
|
promises.push(this._startVideoRecording(screencastOptions));
|
||||||
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
|
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
|
||||||
promises.push(this._firstNonInitialNavigationCommittedPromise);
|
promises.push(this._firstNonInitialNavigationCommittedPromise);
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
@ -824,15 +838,12 @@ class FrameSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onScreencastFrame(payload: Protocol.Page.screencastFramePayload) {
|
_onScreencastFrame(payload: Protocol.Page.screencastFramePayload) {
|
||||||
if (!this._videoRecorder)
|
|
||||||
return;
|
|
||||||
const buffer = Buffer.from(payload.data, 'base64');
|
|
||||||
this._videoRecorder.writeFrame(buffer, payload.metadata.timestamp!);
|
|
||||||
// The target may be closed before receiving the ack.
|
|
||||||
this._client.send('Page.screencastFrameAck', {sessionId: payload.sessionId}).catch(() => {});
|
this._client.send('Page.screencastFrameAck', {sessionId: payload.sessionId}).catch(() => {});
|
||||||
|
const buffer = Buffer.from(payload.data, 'base64');
|
||||||
|
this._page.emit(Page.Events.ScreencastFrame, { buffer, timestamp: payload.metadata.timestamp });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _startVideoRecorder(screencastId: string, options: types.PageScreencastOptions): Promise<void> {
|
async _createVideoRecorder(screencastId: string, options: types.PageScreencastOptions): Promise<void> {
|
||||||
assert(!this._screencastId);
|
assert(!this._screencastId);
|
||||||
const ffmpegPath = this._crPage._browserContext._browser.options.registry.executablePath('ffmpeg');
|
const ffmpegPath = this._crPage._browserContext._browser.options.registry.executablePath('ffmpeg');
|
||||||
if (!ffmpegPath)
|
if (!ffmpegPath)
|
||||||
|
|
@ -857,11 +868,11 @@ class FrameSession {
|
||||||
this._screencastId = screencastId;
|
this._screencastId = screencastId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _startScreencast(options: types.PageScreencastOptions) {
|
async _startVideoRecording(options: types.PageScreencastOptions) {
|
||||||
const screencastId = this._screencastId;
|
const screencastId = this._screencastId;
|
||||||
assert(screencastId);
|
assert(screencastId);
|
||||||
const gotFirstFrame = new Promise(f => this._client.once('Page.screencastFrame', f));
|
const gotFirstFrame = new Promise(f => this._client.once('Page.screencastFrame', f));
|
||||||
await this._client.send('Page.startScreencast', {
|
await this._startScreencast(this._videoRecorder, {
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
quality: 90,
|
quality: 90,
|
||||||
maxWidth: options.width,
|
maxWidth: options.width,
|
||||||
|
|
@ -873,11 +884,11 @@ class FrameSession {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _stopScreencast(): Promise<void> {
|
async _stopVideoRecording(): Promise<void> {
|
||||||
if (!this._screencastId)
|
if (!this._screencastId)
|
||||||
return;
|
return;
|
||||||
await this._client._sendMayFail('Page.stopScreencast');
|
|
||||||
const recorder = this._videoRecorder!;
|
const recorder = this._videoRecorder!;
|
||||||
|
await this._stopScreencast(recorder);
|
||||||
const screencastId = this._screencastId;
|
const screencastId = this._screencastId;
|
||||||
this._videoRecorder = null;
|
this._videoRecorder = null;
|
||||||
this._screencastId = null;
|
this._screencastId = null;
|
||||||
|
|
@ -885,6 +896,18 @@ class FrameSession {
|
||||||
this._crPage._browserContext._browser._videoFinished(screencastId);
|
this._crPage._browserContext._browser._videoFinished(screencastId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _startScreencast(client: any, options: Protocol.Page.startScreencastParameters = {}) {
|
||||||
|
this._screencastClients.add(client);
|
||||||
|
if (this._screencastClients.size === 1)
|
||||||
|
await this._client.send('Page.startScreencast', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _stopScreencast(client: any) {
|
||||||
|
this._screencastClients.delete(client);
|
||||||
|
if (!this._screencastClients.size)
|
||||||
|
await this._client._sendMayFail('Page.stopScreencast');
|
||||||
|
}
|
||||||
|
|
||||||
async _updateExtraHTTPHeaders(initial: boolean): Promise<void> {
|
async _updateExtraHTTPHeaders(initial: boolean): Promise<void> {
|
||||||
const headers = network.mergeHeaders([
|
const headers = network.mergeHeaders([
|
||||||
this._crPage._browserContext._options.extraHTTPHeaders,
|
this._crPage._browserContext._options.extraHTTPHeaders,
|
||||||
|
|
|
||||||
|
|
@ -43,15 +43,16 @@ export class VideoRecorder {
|
||||||
const controller = new ProgressController(internalCallMetadata(), page);
|
const controller = new ProgressController(internalCallMetadata(), page);
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
return await controller.run(async progress => {
|
return await controller.run(async progress => {
|
||||||
const recorder = new VideoRecorder(ffmpegPath, progress);
|
const recorder = new VideoRecorder(page, ffmpegPath, progress);
|
||||||
await recorder._launch(options);
|
await recorder._launch(options);
|
||||||
return recorder;
|
return recorder;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(ffmpegPath: string, progress: Progress) {
|
private constructor(page: Page, ffmpegPath: string, progress: Progress) {
|
||||||
this._progress = progress;
|
this._progress = progress;
|
||||||
this._ffmpegPath = ffmpegPath;
|
this._ffmpegPath = ffmpegPath;
|
||||||
|
page.on(Page.Events.ScreencastFrame, frame => this.writeFrame(frame.buffer, frame.timestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _launch(options: types.PageScreencastOptions) {
|
private async _launch(options: types.PageScreencastOptions) {
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,10 @@ export class FFPage implements PageDelegate {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setScreencastEnabled(enabled: boolean): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
rafCountForStablePosition(): number {
|
rafCountForStablePosition(): number {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export interface PageDelegate {
|
||||||
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
|
||||||
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
|
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
|
||||||
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>;
|
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>;
|
||||||
|
setScreencastEnabled(enabled: boolean): Promise<void>;
|
||||||
|
|
||||||
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
|
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
|
||||||
pdf?: (options?: types.PDFOptions) => Promise<Buffer>;
|
pdf?: (options?: types.PDFOptions) => Promise<Buffer>;
|
||||||
|
|
@ -111,6 +112,7 @@ export class Page extends SdkObject {
|
||||||
FrameDetached: 'framedetached',
|
FrameDetached: 'framedetached',
|
||||||
InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument',
|
InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument',
|
||||||
Load: 'load',
|
Load: 'load',
|
||||||
|
ScreencastFrame: 'screencastframe',
|
||||||
Video: 'video',
|
Video: 'video',
|
||||||
WebSocket: 'websocket',
|
WebSocket: 'websocket',
|
||||||
Worker: 'worker',
|
Worker: 'worker',
|
||||||
|
|
@ -500,6 +502,10 @@ export class Page extends SdkObject {
|
||||||
const identifier = PageBinding.identifier(name, world);
|
const identifier = PageBinding.identifier(name, world);
|
||||||
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setScreencastEnabled(enabled: boolean) {
|
||||||
|
this._delegate.setScreencastEnabled(enabled).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Worker extends SdkObject {
|
export class Worker extends SdkObject {
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,6 @@ import { BaseSnapshotStorage } from './snapshotStorage';
|
||||||
import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
||||||
import { ElementHandle } from '../dom';
|
import { ElementHandle } from '../dom';
|
||||||
|
|
||||||
const kSnapshotInterval = 25;
|
|
||||||
|
|
||||||
export class InMemorySnapshotter extends BaseSnapshotStorage implements SnapshotterDelegate {
|
export class InMemorySnapshotter extends BaseSnapshotStorage implements SnapshotterDelegate {
|
||||||
private _blobs = new Map<string, Buffer>();
|
private _blobs = new Map<string, Buffer>();
|
||||||
private _server: HttpServer;
|
private _server: HttpServer;
|
||||||
|
|
@ -44,10 +42,6 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
||||||
return await this._server.start();
|
return await this._server.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
|
||||||
await this._snapshotter.setAutoSnapshotInterval(kSnapshotInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose() {
|
async dispose() {
|
||||||
this._snapshotter.dispose();
|
this._snapshotter.dispose();
|
||||||
await this._server.stop();
|
await this._server.stop();
|
||||||
|
|
@ -68,7 +62,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAutoSnapshotInterval(interval: number): Promise<void> {
|
async setAutoSnapshotIntervalForTest(interval: number): Promise<void> {
|
||||||
await this._snapshotter.setAutoSnapshotInterval(interval);
|
await this._snapshotter.setAutoSnapshotInterval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,13 @@ export class PersistentSnapshotter extends EventEmitter implements SnapshotterDe
|
||||||
this._snapshotter = new Snapshotter(context, this);
|
this._snapshotter = new Snapshotter(context, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(autoSnapshots: boolean): Promise<void> {
|
||||||
await fsMkdirAsync(this._resourcesDir, {recursive: true}).catch(() => {});
|
await fsMkdirAsync(this._resourcesDir, {recursive: true}).catch(() => {});
|
||||||
await fsAppendFileAsync(this._networkTrace, Buffer.from([]));
|
await fsAppendFileAsync(this._networkTrace, Buffer.from([]));
|
||||||
await fsAppendFileAsync(this._snapshotTrace, Buffer.from([]));
|
await fsAppendFileAsync(this._snapshotTrace, Buffer.from([]));
|
||||||
await this._snapshotter.initialize();
|
await this._snapshotter.initialize();
|
||||||
await this._snapshotter.setAutoSnapshotInterval(kSnapshotInterval);
|
if (autoSnapshots)
|
||||||
|
await this._snapshotter.setAutoSnapshotInterval(kSnapshotInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispose() {
|
async dispose() {
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ export class Snapshotter {
|
||||||
this._eventListeners.push(helper.addEventListener(page, Page.Events.Response, (response: network.Response) => {
|
this._eventListeners.push(helper.addEventListener(page, Page.Events.Response, (response: network.Response) => {
|
||||||
this._saveResource(page, response).catch(e => debugLogger.log('error', e));
|
this._saveResource(page, response).catch(e => debugLogger.log('error', e));
|
||||||
}));
|
}));
|
||||||
|
page.setScreencastEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _saveResource(page: Page, response: network.Response) {
|
private async _saveResource(page: Page, response: network.Response) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,15 @@ export type PageDestroyedTraceEvent = {
|
||||||
pageId: string,
|
pageId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ScreencastFrameTraceEvent = {
|
||||||
|
timestamp: number,
|
||||||
|
type: 'page-screencast-frame',
|
||||||
|
contextId: string,
|
||||||
|
pageId: string,
|
||||||
|
pageTimestamp: number,
|
||||||
|
sha1: string
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionTraceEvent = {
|
export type ActionTraceEvent = {
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
type: 'action',
|
type: 'action',
|
||||||
|
|
@ -93,6 +102,7 @@ export type TraceEvent =
|
||||||
ContextDestroyedTraceEvent |
|
ContextDestroyedTraceEvent |
|
||||||
PageCreatedTraceEvent |
|
PageCreatedTraceEvent |
|
||||||
PageDestroyedTraceEvent |
|
PageDestroyedTraceEvent |
|
||||||
|
ScreencastFrameTraceEvent |
|
||||||
ActionTraceEvent |
|
ActionTraceEvent |
|
||||||
DialogOpenedEvent |
|
DialogOpenedEvent |
|
||||||
DialogClosedEvent |
|
DialogClosedEvent |
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
|
import { calculateSha1, createGuid, getFromENV, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
|
||||||
import { BrowserContext } from '../../browserContext';
|
import { BrowserContext } from '../../browserContext';
|
||||||
import { Dialog } from '../../dialog';
|
import { Dialog } from '../../dialog';
|
||||||
import { ElementHandle } from '../../dom';
|
import { ElementHandle } from '../../dom';
|
||||||
|
|
@ -105,7 +105,7 @@ class ContextTracer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
await this._snapshotter.start();
|
await this._snapshotter.start(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _captureSnapshot(name: 'before' | 'after' | 'action', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
async _captureSnapshot(name: 'before' | 'after' | 'action', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
||||||
|
|
@ -193,6 +193,20 @@ class ContextTracer {
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
page.on(Page.Events.ScreencastFrame, params => {
|
||||||
|
const sha1 = calculateSha1(params.buffer);
|
||||||
|
const event: trace.ScreencastFrameTraceEvent = {
|
||||||
|
type: 'page-screencast-frame',
|
||||||
|
pageId: page.uniqueId,
|
||||||
|
contextId: this._contextId,
|
||||||
|
sha1,
|
||||||
|
pageTimestamp: params.timestamp,
|
||||||
|
timestamp: monotonicTime()
|
||||||
|
};
|
||||||
|
this._appendTraceEvent(event);
|
||||||
|
this._snapshotter.onBlob({ sha1, buffer: params.buffer });
|
||||||
|
});
|
||||||
|
|
||||||
page.once(Page.Events.Close, () => {
|
page.once(Page.Events.Close, () => {
|
||||||
if (this._disposed)
|
if (this._disposed)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export class TraceModel {
|
||||||
destroyed: undefined as any,
|
destroyed: undefined as any,
|
||||||
actions: [],
|
actions: [],
|
||||||
interestingEvents: [],
|
interestingEvents: [],
|
||||||
|
screencastFrames: [],
|
||||||
};
|
};
|
||||||
const contextEntry = this.contextEntries.get(event.contextId)!;
|
const contextEntry = this.contextEntries.get(event.contextId)!;
|
||||||
this.pageEntries.set(event.pageId, { pageEntry, contextEntry });
|
this.pageEntries.set(event.pageId, { pageEntry, contextEntry });
|
||||||
|
|
@ -78,6 +79,10 @@ export class TraceModel {
|
||||||
this.pageEntries.get(event.pageId)!.pageEntry.destroyed = event;
|
this.pageEntries.get(event.pageId)!.pageEntry.destroyed = event;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'page-screencast-frame': {
|
||||||
|
this.pageEntries.get(event.pageId)!.pageEntry.screencastFrames.push(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'action': {
|
case 'action': {
|
||||||
const metadata = event.metadata;
|
const metadata = event.metadata;
|
||||||
if (metadata.method === 'waitForEventInfo')
|
if (metadata.method === 'waitForEventInfo')
|
||||||
|
|
@ -145,6 +150,7 @@ export type PageEntry = {
|
||||||
destroyed: trace.PageDestroyedTraceEvent;
|
destroyed: trace.PageDestroyedTraceEvent;
|
||||||
actions: ActionEntry[];
|
actions: ActionEntry[];
|
||||||
interestingEvents: InterestingPageEvent[];
|
interestingEvents: InterestingPageEvent[];
|
||||||
|
screencastFrames: { sha1: string, timestamp: number }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActionEntry = trace.ActionTraceEvent & {
|
export type ActionEntry = trace.ActionTraceEvent & {
|
||||||
|
|
|
||||||
|
|
@ -819,6 +819,10 @@ export class WKPage implements PageDelegate {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setScreencastEnabled(enabled: boolean): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
rafCountForStablePosition(): number {
|
rafCountForStablePosition(): number {
|
||||||
return process.platform === 'win32' ? 5 : 1;
|
return process.platform === 'win32' ? 5 : 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/web/traceViewer/ui/filmStrip.css
Normal file
45
src/web/traceViewer/ui/filmStrip.css
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.film-strip {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.film-strip-lane {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.film-strip-frame {
|
||||||
|
flex: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.film-strip-hover {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 10px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 10px 0px;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
111
src/web/traceViewer/ui/filmStrip.tsx
Normal file
111
src/web/traceViewer/ui/filmStrip.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './filmStrip.css';
|
||||||
|
import { Boundaries, Size } from '../geometry';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useMeasure } from './helpers';
|
||||||
|
import { lowerBound } from '../../uiUtils';
|
||||||
|
import { ContextEntry, PageEntry } from '../../../server/trace/viewer/traceModel';
|
||||||
|
|
||||||
|
export const FilmStrip: React.FunctionComponent<{
|
||||||
|
context: ContextEntry,
|
||||||
|
boundaries: Boundaries,
|
||||||
|
previewX?: number,
|
||||||
|
}> = ({ context, boundaries, previewX }) => {
|
||||||
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
|
|
||||||
|
const screencastFrames = context.pages[0]?.screencastFrames;
|
||||||
|
// TODO: pick file from the Y position.
|
||||||
|
let previewImage = undefined;
|
||||||
|
if (previewX !== undefined && context.pages.length) {
|
||||||
|
const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewX / measure.width;
|
||||||
|
previewImage = screencastFrames[lowerBound(screencastFrames, previewTime, timeComparator)];
|
||||||
|
}
|
||||||
|
const previewSize = inscribe(context.created.viewportSize!, { width: 600, height: 600 });
|
||||||
|
console.log(previewSize);
|
||||||
|
|
||||||
|
return <div className='film-strip' ref={ref}>{
|
||||||
|
context.pages.filter(p => p.screencastFrames.length).map((page, index) => <FilmStripLane
|
||||||
|
boundaries={boundaries}
|
||||||
|
viewportSize={context.created.viewportSize!}
|
||||||
|
page={page}
|
||||||
|
width={measure.width}
|
||||||
|
key={index}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
{previewImage && previewX !== undefined &&
|
||||||
|
<div className='film-strip-hover' style={{
|
||||||
|
width: previewSize.width,
|
||||||
|
height: previewSize.height,
|
||||||
|
top: measure.bottom + 5,
|
||||||
|
left: Math.min(previewX, measure.width - previewSize.width - 10),
|
||||||
|
}}>
|
||||||
|
<img src={`/sha1/${previewImage.sha1}`} width={previewSize.width} height={previewSize.height} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilmStripLane: React.FunctionComponent<{
|
||||||
|
boundaries: Boundaries,
|
||||||
|
viewportSize: Size,
|
||||||
|
page: PageEntry,
|
||||||
|
width: number,
|
||||||
|
}> = ({ boundaries, viewportSize, page, width }) => {
|
||||||
|
const frameSize = inscribe(viewportSize!, { width: 200, height: 45 });
|
||||||
|
const frameMargin = 2.5;
|
||||||
|
const screencastFrames = page.screencastFrames;
|
||||||
|
const startTime = screencastFrames[0].timestamp;
|
||||||
|
const endTime = screencastFrames[screencastFrames.length - 1].timestamp;
|
||||||
|
|
||||||
|
const boundariesDuration = boundaries.maximum - boundaries.minimum;
|
||||||
|
const gapLeft = (startTime - boundaries.minimum) / boundariesDuration * width;
|
||||||
|
const gapRight = (boundaries.maximum - endTime) / boundariesDuration * width;
|
||||||
|
const effectiveWidth = (endTime - startTime) / boundariesDuration * width;
|
||||||
|
const frameCount = effectiveWidth / (frameSize.width + 2 * frameMargin) | 0;
|
||||||
|
const frameDuration = (endTime - startTime) / frameCount;
|
||||||
|
|
||||||
|
const frames: JSX.Element[] = [];
|
||||||
|
for (let time = startTime, i = 0; time <= endTime; time += frameDuration, ++i) {
|
||||||
|
const index = lowerBound(screencastFrames, time, timeComparator);
|
||||||
|
frames.push(<div className='film-strip-frame' key={i} style={{
|
||||||
|
width: frameSize.width,
|
||||||
|
height: frameSize.height,
|
||||||
|
backgroundImage: `url(/sha1/${screencastFrames[index].sha1})`,
|
||||||
|
backgroundSize: `${frameSize.width}px ${frameSize.height}px`,
|
||||||
|
margin: frameMargin,
|
||||||
|
marginRight: frameMargin,
|
||||||
|
}} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='film-strip-lane' style={{
|
||||||
|
marginLeft: gapLeft + 'px',
|
||||||
|
marginRight: gapRight + 'px',
|
||||||
|
}}>{frames}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function timeComparator(time: number, frame: { timestamp: number }): number {
|
||||||
|
return time - frame.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inscribe(object: Size, area: Size): Size {
|
||||||
|
const scale = Math.max(object.width / area.width, object.height / area.height);
|
||||||
|
return {
|
||||||
|
width: object.width / scale | 0,
|
||||||
|
height: object.height / scale | 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -15,35 +15,28 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
||||||
import { Boundaries, Size } from '../geometry';
|
import { Size } from '../geometry';
|
||||||
import './snapshotTab.css';
|
import './snapshotTab.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import { msToString } from '../../uiUtils';
|
|
||||||
import type { Point } from '../../../common/types';
|
import type { Point } from '../../../common/types';
|
||||||
|
|
||||||
export const SnapshotTab: React.FunctionComponent<{
|
export const SnapshotTab: React.FunctionComponent<{
|
||||||
actionEntry: ActionEntry | undefined,
|
actionEntry: ActionEntry | undefined,
|
||||||
snapshotSize: Size,
|
snapshotSize: Size,
|
||||||
selection: { pageId: string, time: number } | undefined,
|
}> = ({ actionEntry, snapshotSize }) => {
|
||||||
boundaries: Boundaries,
|
|
||||||
}> = ({ actionEntry, snapshotSize, selection, boundaries }) => {
|
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||||
|
|
||||||
const snapshots = actionEntry ? (actionEntry.snapshots || []) : [];
|
const snapshots = actionEntry ? (actionEntry.snapshots || []) : [];
|
||||||
const { pageId, time } = selection || { pageId: undefined, time: 0 };
|
|
||||||
|
|
||||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!iframeRef.current)
|
if (!iframeRef.current)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let snapshotUri = undefined;
|
let snapshotUri = undefined;
|
||||||
let point: Point | undefined = undefined;
|
let point: Point | undefined = undefined;
|
||||||
if (pageId) {
|
if (actionEntry) {
|
||||||
snapshotUri = `${pageId}?time=${time}`;
|
|
||||||
} else if (actionEntry) {
|
|
||||||
const snapshot = snapshots[snapshotIndex];
|
const snapshot = snapshots[snapshotIndex];
|
||||||
if (snapshot && snapshot.snapshotName) {
|
if (snapshot && snapshot.snapshotName) {
|
||||||
snapshotUri = `${actionEntry.metadata.pageId}?name=${snapshot.snapshotName}`;
|
snapshotUri = `${actionEntry.metadata.pageId}?name=${snapshot.snapshotName}`;
|
||||||
|
|
@ -56,7 +49,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
|
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
}, [actionEntry, snapshotIndex, pageId, time]);
|
}, [actionEntry, snapshotIndex]);
|
||||||
|
|
||||||
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
||||||
const scaledSize = {
|
const scaledSize = {
|
||||||
|
|
@ -64,11 +57,8 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
height: snapshotSize.height * scale,
|
height: snapshotSize.height * scale,
|
||||||
};
|
};
|
||||||
return <div className='snapshot-tab'>
|
return <div className='snapshot-tab'>
|
||||||
<div className='snapshot-controls'>{
|
<div className='snapshot-controls'>
|
||||||
selection && <div key='selectedTime' className='snapshot-toggle'>
|
{snapshots.map((snapshot, index) => {
|
||||||
{msToString(selection.time - boundaries.minimum)}
|
|
||||||
</div>
|
|
||||||
}{!selection && snapshots.map((snapshot, index) => {
|
|
||||||
return <div
|
return <div
|
||||||
key={snapshot.title}
|
key={snapshot.title}
|
||||||
className={'snapshot-toggle' + (snapshotIndex === index ? ' toggled' : '')}
|
className={'snapshot-toggle' + (snapshotIndex === index ? ' toggled' : '')}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { Boundaries } from '../geometry';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import { msToString } from '../../uiUtils';
|
import { msToString } from '../../uiUtils';
|
||||||
|
import { FilmStrip } from './filmStrip';
|
||||||
|
|
||||||
type TimelineBar = {
|
type TimelineBar = {
|
||||||
entry?: ActionEntry;
|
entry?: ActionEntry;
|
||||||
|
|
@ -40,8 +41,7 @@ export const Timeline: React.FunctionComponent<{
|
||||||
selectedAction: ActionEntry | undefined,
|
selectedAction: ActionEntry | undefined,
|
||||||
highlightedAction: ActionEntry | undefined,
|
highlightedAction: ActionEntry | undefined,
|
||||||
onSelected: (action: ActionEntry) => void,
|
onSelected: (action: ActionEntry) => void,
|
||||||
onTimeSelected: (time: number | undefined) => void,
|
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected }) => {
|
||||||
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onTimeSelected }) => {
|
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
||||||
const [hoveredBarIndex, setHoveredBarIndex] = React.useState<number | undefined>();
|
const [hoveredBarIndex, setHoveredBarIndex] = React.useState<number | undefined>();
|
||||||
|
|
@ -143,12 +143,10 @@ export const Timeline: React.FunctionComponent<{
|
||||||
return;
|
return;
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
setPreviewX(x);
|
setPreviewX(x);
|
||||||
onTimeSelected(positionToTime(measure.width, boundaries, x));
|
|
||||||
setHoveredBarIndex(findHoveredBarIndex(x));
|
setHoveredBarIndex(findHoveredBarIndex(x));
|
||||||
};
|
};
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
setPreviewX(undefined);
|
setPreviewX(undefined);
|
||||||
onTimeSelected(undefined);
|
|
||||||
};
|
};
|
||||||
const onClick = (event: React.MouseEvent) => {
|
const onClick = (event: React.MouseEvent) => {
|
||||||
if (!ref.current)
|
if (!ref.current)
|
||||||
|
|
@ -194,6 +192,7 @@ export const Timeline: React.FunctionComponent<{
|
||||||
></div>;
|
></div>;
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
|
<FilmStrip context={context} boundaries={boundaries} previewX={previewX} />
|
||||||
<div className='timeline-marker timeline-marker-hover' style={{
|
<div className='timeline-marker timeline-marker-hover' style={{
|
||||||
display: (previewX !== undefined) ? 'block' : 'none',
|
display: (previewX !== undefined) ? 'block' : 'none',
|
||||||
left: (previewX || 0) + 'px',
|
left: (previewX || 0) + 'px',
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const [context, setContext] = React.useState(contexts[0]);
|
const [context, setContext] = React.useState(contexts[0]);
|
||||||
const [selectedAction, setSelectedAction] = React.useState<ActionEntry | undefined>();
|
const [selectedAction, setSelectedAction] = React.useState<ActionEntry | undefined>();
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>();
|
||||||
const [selectedTime, setSelectedTime] = React.useState<number | undefined>();
|
|
||||||
|
|
||||||
const actions = React.useMemo(() => {
|
const actions = React.useMemo(() => {
|
||||||
const actions: ActionEntry[] = [];
|
const actions: ActionEntry[] = [];
|
||||||
|
|
@ -45,7 +44,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
|
|
||||||
const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
|
const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
|
||||||
const boundaries = { minimum: context.startTime, maximum: context.endTime };
|
const boundaries = { minimum: context.startTime, maximum: context.endTime };
|
||||||
const snapshotSelection = context.pages.length && selectedTime !== undefined ? { pageId: context.pages[0].created.pageId, time: selectedTime } : undefined;
|
|
||||||
|
|
||||||
return <div className='vbox workbench'>
|
return <div className='vbox workbench'>
|
||||||
<div className='hbox header'>
|
<div className='hbox header'>
|
||||||
|
|
@ -58,7 +56,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
onChange={context => {
|
onChange={context => {
|
||||||
setContext(context);
|
setContext(context);
|
||||||
setSelectedAction(undefined);
|
setSelectedAction(undefined);
|
||||||
setSelectedTime(undefined);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,12 +66,11 @@ export const Workbench: React.FunctionComponent<{
|
||||||
selectedAction={selectedAction}
|
selectedAction={selectedAction}
|
||||||
highlightedAction={highlightedAction}
|
highlightedAction={highlightedAction}
|
||||||
onSelected={action => setSelectedAction(action)}
|
onSelected={action => setSelectedAction(action)}
|
||||||
onTimeSelected={time => setSelectedTime(time)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
<SplitView sidebarSize={250}>
|
<SplitView sidebarSize={250}>
|
||||||
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} />
|
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} />
|
||||||
<TabbedPane tabs={[
|
<TabbedPane tabs={[
|
||||||
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
|
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
|
||||||
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
||||||
|
|
@ -87,7 +83,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
highlightedAction={highlightedAction}
|
highlightedAction={highlightedAction}
|
||||||
onSelected={action => {
|
onSelected={action => {
|
||||||
setSelectedAction(action);
|
setSelectedAction(action);
|
||||||
setSelectedTime(undefined);
|
|
||||||
}}
|
}}
|
||||||
onHighlighted={action => setHighlightedAction(action)}
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,29 @@ export function msToString(ms: number): string {
|
||||||
const days = hours / 24;
|
const days = hours / 24;
|
||||||
return days.toFixed(1) + 'd';
|
return days.toFixed(1) + 'd';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lowerBound<S, T>(array: S[], object: T, comparator: (object: T, b: S) => number, left?: number, right?: number): number {
|
||||||
|
let l = left || 0;
|
||||||
|
let r = right !== undefined ? right : array.length;
|
||||||
|
while (l < r) {
|
||||||
|
const m = (l + r) >> 1;
|
||||||
|
if (comparator(object, array[m]) > 0)
|
||||||
|
l = m + 1;
|
||||||
|
else
|
||||||
|
r = m;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upperBound<S, T>(array: S[], object: T, comparator: (object: T, b: S) => number, left?: number, right?: number): number {
|
||||||
|
let l = left || 0;
|
||||||
|
let r = right !== undefined ? right : array.length;
|
||||||
|
while (l < r) {
|
||||||
|
const m = (l + r) >> 1;
|
||||||
|
if (comparator(object, array[m]) >= 0)
|
||||||
|
l = m + 1;
|
||||||
|
else
|
||||||
|
r = m;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ it.describe('snapshots', () => {
|
||||||
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot));
|
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot));
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
new Promise(f => snapshotter.once('snapshot', f)),
|
new Promise(f => snapshotter.once('snapshot', f)),
|
||||||
snapshotter.setAutoSnapshotInterval(25),
|
snapshotter.setAutoSnapshotIntervalForTest(25),
|
||||||
]);
|
]);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
new Promise(f => snapshotter.once('snapshot', f)),
|
new Promise(f => snapshotter.once('snapshot', f)),
|
||||||
|
|
@ -88,7 +88,7 @@ it.describe('snapshots', () => {
|
||||||
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot));
|
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot));
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
new Promise(f => snapshotter.once('snapshot', f)),
|
new Promise(f => snapshotter.once('snapshot', f)),
|
||||||
snapshotter.setAutoSnapshotInterval(25),
|
snapshotter.setAutoSnapshotIntervalForTest(25),
|
||||||
]);
|
]);
|
||||||
expect(distillSnapshot(snapshots[0])).toBe('<style>button { color: red; }</style><BUTTON>Hello</BUTTON>');
|
expect(distillSnapshot(snapshots[0])).toBe('<style>button { color: red; }</style><BUTTON>Hello</BUTTON>');
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ it.describe('snapshots', () => {
|
||||||
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot));
|
snapshotter.on('snapshot', snapshot => snapshots.push(snapshot));
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
new Promise(f => snapshotter.once('snapshot', f)),
|
new Promise(f => snapshotter.once('snapshot', f)),
|
||||||
snapshotter.setAutoSnapshotInterval(25),
|
snapshotter.setAutoSnapshotIntervalForTest(25),
|
||||||
]);
|
]);
|
||||||
expect(distillSnapshot(snapshots[0])).toBe('<LINK rel=\"stylesheet\" href=\"style.css\"><BUTTON>Hello</BUTTON>');
|
expect(distillSnapshot(snapshots[0])).toBe('<LINK rel=\"stylesheet\" href=\"style.css\"><BUTTON>Hello</BUTTON>');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue