chore: remove hierarchy of SnapshotStorage (#21853)

This commit is contained in:
Dmitry Gozman 2023-03-22 09:32:21 -07:00 committed by GitHub
parent e7d670e27b
commit a01fd04d63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 46 additions and 85 deletions

View File

@ -18,23 +18,27 @@ import type { BrowserContext } from '../../browserContext';
import type { Page } from '../../page';
import type { FrameSnapshot } from '@trace/snapshot';
import type { SnapshotRenderer } from '../../../../../trace-viewer/src/snapshotRenderer';
import { BaseSnapshotStorage } from '../../../../../trace-viewer/src/snapshotStorage';
import { SnapshotStorage } from '../../../../../trace-viewer/src/snapshotStorage';
import type { SnapshotterBlob, SnapshotterDelegate } from '../recorder/snapshotter';
import { Snapshotter } from '../recorder/snapshotter';
import type { ElementHandle } from '../../dom';
import type { HarTracerDelegate } from '../../har/harTracer';
import { HarTracer } from '../../har/harTracer';
import type * as har from '@trace/har';
import { ManualPromise } from '../../../utils';
export class InMemorySnapshotter extends BaseSnapshotStorage implements SnapshotterDelegate, HarTracerDelegate {
export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelegate {
private _blobs = new Map<string, Buffer>();
private _snapshotter: Snapshotter;
private _harTracer: HarTracer;
private _snapshotReadyPromises = new Map<string, ManualPromise<SnapshotRenderer>>();
private _storage: SnapshotStorage;
private _snapshotCount = 0;
constructor(context: BrowserContext) {
super();
this._snapshotter = new Snapshotter(context, this);
this._harTracer = new HarTracer(context, null, this, { content: 'attach', includeTraceInfo: true, recordRequestOverrides: false, waitForContentOnStop: false, skipScripts: true });
this._storage = new SnapshotStorage();
}
async initialize(): Promise<void> {
@ -47,7 +51,6 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
await this._harTracer.flush();
this._harTracer.stop();
this._harTracer.start();
this.clear();
}
async dispose() {
@ -57,25 +60,20 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
}
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<SnapshotRenderer> {
if (this._frameSnapshots.has(snapshotName))
if (this._snapshotReadyPromises.has(snapshotName))
throw new Error('Duplicate snapshot name: ' + snapshotName);
this._snapshotter.captureSnapshot(page, callId, snapshotName, element).catch(() => {});
return new Promise<SnapshotRenderer>(fulfill => {
const disposable = this.onSnapshotEvent((renderer: SnapshotRenderer) => {
if (renderer.snapshotName === snapshotName) {
disposable.dispose();
fulfill(renderer);
}
});
});
const promise = new ManualPromise<SnapshotRenderer>();
this._snapshotReadyPromises.set(snapshotName, promise);
return promise;
}
onEntryStarted(entry: har.Entry) {
}
onEntryFinished(entry: har.Entry) {
this.addResource(entry);
this._storage.addResource(entry);
}
onContentBlob(sha1: string, buffer: Buffer) {
@ -87,14 +85,16 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
}
onFrameSnapshot(snapshot: FrameSnapshot): void {
this.addFrameSnapshot(snapshot);
}
async resourceContent(sha1: string): Promise<Blob | undefined> {
throw new Error('Not implemented');
++this._snapshotCount;
const renderer = this._storage.addFrameSnapshot(snapshot);
this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer);
}
async resourceContentForTest(sha1: string): Promise<Buffer | undefined> {
return this._blobs.get(sha1);
}
snapshotCount() {
return this._snapshotCount;
}
}

View File

@ -20,7 +20,7 @@ export class SnapshotRenderer {
private _snapshots: FrameSnapshot[];
private _index: number;
readonly snapshotName: string | undefined;
_resources: ResourceSnapshot[];
private _resources: ResourceSnapshot[];
private _snapshot: FrameSnapshot;
private _callId: string;

View File

@ -14,19 +14,21 @@
* limitations under the License.
*/
import type { SnapshotStorage } from './snapshotStorage';
import type { URLSearchParams } from 'url';
import type { SnapshotRenderer } from './snapshotRenderer';
import type { SnapshotStorage } from './snapshotStorage';
import type { ResourceSnapshot } from '@trace/snapshot';
type Point = { x: number, y: number };
export class SnapshotServer {
private _snapshotStorage: SnapshotStorage;
private _resourceLoader: (sha1: string) => Promise<Blob | undefined>;
private _snapshotIds = new Map<string, SnapshotRenderer>();
constructor(snapshotStorage: SnapshotStorage) {
constructor(snapshotStorage: SnapshotStorage, resourceLoader: (sha1: string) => Promise<Blob | undefined>) {
this._snapshotStorage = snapshotStorage;
this._resourceLoader = resourceLoader;
}
serveSnapshot(pathname: string, searchParams: URLSearchParams, snapshotUrl: string): Response {
@ -75,7 +77,7 @@ export class SnapshotServer {
return new Response(null, { status: 404 });
const sha1 = resource.response.content._sha1;
const content = sha1 ? await this._snapshotStorage.resourceContent(sha1) || new Blob([]) : new Blob([]);
const content = sha1 ? await this._resourceLoader(sha1) || new Blob([]) : new Blob([]);
let contentType = resource.response.content.mimeType;
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType);

View File

@ -15,43 +15,28 @@
*/
import type { FrameSnapshot, ResourceSnapshot } from '@trace/snapshot';
import { EventEmitter } from './events';
import { rewriteURLForCustomProtocol, SnapshotRenderer } from './snapshotRenderer';
export interface SnapshotStorage {
resources(): ResourceSnapshot[];
resourceContent(sha1: string): Promise<Blob | undefined>;
snapshotByName(pageOrFrameId: string, snapshotName: string): SnapshotRenderer | undefined;
snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined;
}
export abstract class BaseSnapshotStorage implements SnapshotStorage {
protected _resources: ResourceSnapshot[] = [];
protected _frameSnapshots = new Map<string, {
export class SnapshotStorage {
private _resources: ResourceSnapshot[] = [];
private _frameSnapshots = new Map<string, {
raw: FrameSnapshot[],
renderer: SnapshotRenderer[]
renderers: SnapshotRenderer[]
}>();
private _didSnapshot = new EventEmitter<SnapshotRenderer>();
readonly onSnapshotEvent = this._didSnapshot.event;
clear() {
this._resources = [];
this._frameSnapshots.clear();
}
addResource(resource: ResourceSnapshot): void {
resource.request.url = rewriteURLForCustomProtocol(resource.request.url);
this._resources.push(resource);
}
addFrameSnapshot(snapshot: FrameSnapshot): void {
addFrameSnapshot(snapshot: FrameSnapshot) {
for (const override of snapshot.resourceOverrides)
override.url = rewriteURLForCustomProtocol(override.url);
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
if (!frameSnapshots) {
frameSnapshots = {
raw: [],
renderer: [],
renderers: [],
};
this._frameSnapshots.set(snapshot.frameId, frameSnapshots);
if (snapshot.isMainFrame)
@ -59,23 +44,12 @@ export abstract class BaseSnapshotStorage implements SnapshotStorage {
}
frameSnapshots.raw.push(snapshot);
const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1);
frameSnapshots.renderer.push(renderer);
this._didSnapshot.fire(renderer);
}
abstract resourceContent(sha1: string): Promise<Blob | undefined>;
resources(): ResourceSnapshot[] {
return this._resources.slice();
frameSnapshots.renderers.push(renderer);
return renderer;
}
snapshotByName(pageOrFrameId: string, snapshotName: string): SnapshotRenderer | undefined {
const snapshot = this._frameSnapshots.get(pageOrFrameId);
return snapshot?.renderer.find(r => r.snapshotName === snapshotName);
}
snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined {
const snapshot = this._frameSnapshots.get(frameId);
return snapshot?.renderer[index];
return snapshot?.renderers.find(r => r.snapshotName === snapshotName);
}
}

View File

@ -49,7 +49,7 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
else
throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`);
}
const snapshotServer = new SnapshotServer(traceModel.storage());
const snapshotServer = new SnapshotServer(traceModel.storage(), sha1 => traceModel.resourceForSha1(sha1));
loadedTraces.set(traceUrl, { traceModel, snapshotServer });
return traceModel;
}

View File

@ -22,14 +22,14 @@ import type zip from '@zip.js/zip.js';
import zipImport from '@zip.js/zip.js/dist/zip-no-worker-inflate.min.js';
import type { ContextEntry, PageEntry } from './entries';
import { createEmptyContext } from './entries';
import { BaseSnapshotStorage } from './snapshotStorage';
import { SnapshotStorage } from './snapshotStorage';
const zipjs = zipImport as typeof zip;
export class TraceModel {
contextEntries: ContextEntry[] = [];
pageEntries = new Map<string, PageEntry>();
private _snapshotStorage: BaseSnapshotStorage | undefined;
private _snapshotStorage: SnapshotStorage | undefined;
private _version: number | undefined;
private _backend!: TraceModelBackend;
@ -52,7 +52,7 @@ export class TraceModel {
if (!ordinals.length)
throw new Error('Cannot find .trace file');
this._snapshotStorage = new PersistentSnapshotStorage(this._backend);
this._snapshotStorage = new SnapshotStorage();
for (const ordinal of ordinals) {
const contextEntry = createEmptyContext();
@ -95,7 +95,7 @@ export class TraceModel {
return this._backend.readBlob('resources/' + sha1);
}
storage(): BaseSnapshotStorage {
storage(): SnapshotStorage {
return this._snapshotStorage!;
}
@ -387,19 +387,6 @@ class FetchTraceModelBackend implements TraceModelBackend {
}
}
export class PersistentSnapshotStorage extends BaseSnapshotStorage {
private _backend: TraceModelBackend;
constructor(backend: TraceModelBackend) {
super();
this._backend = backend;
}
async resourceContent(sha1: string): Promise<Blob | undefined> {
return this._backend.readBlob('resources/' + sha1);
}
}
function formatUrl(trace: string) {
let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${trace}`;
// Dropbox does not support cors.

View File

@ -57,11 +57,9 @@ it.describe('snapshots', () => {
it('should collect multiple', async ({ page, toImpl, snapshotter }) => {
await page.setContent('<button>Hello</button>');
const snapshots = [];
snapshotter.onSnapshotEvent(snapshot => snapshots.push(snapshot));
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);
expect(snapshotter.snapshotCount()).toBe(2);
});
it('should respect inline CSSOM change', async ({ page, toImpl, snapshotter }) => {
@ -88,7 +86,7 @@ it.describe('snapshots', () => {
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), 'snapshot@call@2');
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
expect(distillSnapshot(snapshot2)).toBe('<DIV id=\"div\" attr1=\"1\"></DIV>');
});
@ -125,7 +123,7 @@ it.describe('snapshots', () => {
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), 'call@1', 'snapshot@call@1');
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
const resource = snapshot2.resourceByUrl(`http://localhost:${server.PORT}/style.css`);
expect((await snapshotter.resourceContentForTest(resource.response.content._sha1)).toString()).toBe('button { color: blue; }');
});
@ -171,7 +169,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;
@ -227,7 +225,7 @@ it.describe('snapshots', () => {
}
await handle.evaluate(element => element.setAttribute('data', 'two'));
{
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot@call@2');
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@3', 'snapshot@call@3');
expect(distillSnapshot(snapshot)).toBe('<BUTTON data="two">Hello</BUTTON>');
}
});
@ -251,11 +249,11 @@ it.describe('snapshots', () => {
}
});
const renderer1 = await snapshotter.captureSnapshot(toImpl(page), 'call1', 'snapshot@call@1');
const renderer1 = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1');
// Expect some adopted style sheets.
expect(distillSnapshot(renderer1)).toContain('__playwright_style_sheet_');
const renderer2 = await snapshotter.captureSnapshot(toImpl(page), 'call2', 'snapshot@call@2');
const renderer2 = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2');
const snapshot2 = renderer2.snapshot();
// Second snapshot should be just a copy of the first one.
expect(snapshot2.html).toEqual([[1, 13]]);