166 lines
5.7 KiB
TypeScript
166 lines
5.7 KiB
TypeScript
/**
|
|
* 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 { BrowserContext } from '../browserContext';
|
|
import { Page } from '../page';
|
|
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
|
|
import { debugLogger } from '../../utils/debugLogger';
|
|
import { Frame } from '../frames';
|
|
import { frameSnapshotStreamer, SnapshotData } from './snapshotterInjected';
|
|
import { calculateSha1, createGuid, monotonicTime } from '../../utils/utils';
|
|
import { FrameSnapshot } from './snapshotTypes';
|
|
import { ElementHandle } from '../dom';
|
|
import * as mime from 'mime';
|
|
|
|
export type SnapshotterBlob = {
|
|
buffer: Buffer,
|
|
sha1: string,
|
|
};
|
|
|
|
export interface SnapshotterDelegate {
|
|
onSnapshotterBlob(blob: SnapshotterBlob): void;
|
|
onFrameSnapshot(snapshot: FrameSnapshot): void;
|
|
}
|
|
|
|
export class Snapshotter {
|
|
private _context: BrowserContext;
|
|
private _delegate: SnapshotterDelegate;
|
|
private _eventListeners: RegisteredListener[] = [];
|
|
private _snapshotStreamer: string;
|
|
private _initialized = false;
|
|
private _started = false;
|
|
|
|
constructor(context: BrowserContext, delegate: SnapshotterDelegate) {
|
|
this._context = context;
|
|
this._delegate = delegate;
|
|
const guid = createGuid();
|
|
this._snapshotStreamer = '__playwright_snapshot_streamer_' + guid;
|
|
}
|
|
|
|
started(): boolean {
|
|
return this._started;
|
|
}
|
|
|
|
async start() {
|
|
this._started = true;
|
|
if (!this._initialized) {
|
|
this._initialized = true;
|
|
await this._initialize();
|
|
}
|
|
await this.reset();
|
|
}
|
|
|
|
async reset() {
|
|
if (this._started)
|
|
await this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`);
|
|
}
|
|
|
|
async stop() {
|
|
this._started = false;
|
|
}
|
|
|
|
async _initialize() {
|
|
for (const page of this._context.pages())
|
|
this._onPage(page);
|
|
this._eventListeners = [
|
|
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
|
|
];
|
|
|
|
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}")`;
|
|
await this._context._doAddInitScript(initScript);
|
|
await this._runInAllFrames(initScript);
|
|
}
|
|
|
|
private async _runInAllFrames(expression: string) {
|
|
const frames = [];
|
|
for (const page of this._context.pages())
|
|
frames.push(...page.frames());
|
|
await Promise.all(frames.map(frame => {
|
|
return frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e));
|
|
}));
|
|
}
|
|
|
|
dispose() {
|
|
eventsHelper.removeEventListeners(this._eventListeners);
|
|
}
|
|
|
|
async captureSnapshot(page: Page, 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);
|
|
|
|
// In each frame, in a non-stalling manner, capture the snapshots.
|
|
const snapshots = page.frames().map(async frame => {
|
|
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData;
|
|
// Something went wrong -> bail out, our snapshots are best-efforty.
|
|
if (!data || !this._started)
|
|
return;
|
|
|
|
const snapshot: FrameSnapshot = {
|
|
snapshotName,
|
|
pageId: page.guid,
|
|
frameId: frame.guid,
|
|
frameUrl: data.url,
|
|
doctype: data.doctype,
|
|
html: data.html,
|
|
viewport: data.viewport,
|
|
timestamp: monotonicTime(),
|
|
collectionTime: data.collectionTime,
|
|
resourceOverrides: [],
|
|
isMainFrame: page.mainFrame() === frame
|
|
};
|
|
for (const { url, content, contentType } of data.resourceOverrides) {
|
|
if (typeof content === 'string') {
|
|
const buffer = Buffer.from(content);
|
|
const sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(contentType) || 'dat');
|
|
this._delegate.onSnapshotterBlob({ sha1, buffer });
|
|
snapshot.resourceOverrides.push({ url, sha1 });
|
|
} else {
|
|
snapshot.resourceOverrides.push({ url, ref: content });
|
|
}
|
|
}
|
|
this._delegate.onFrameSnapshot(snapshot);
|
|
});
|
|
await Promise.all(snapshots);
|
|
}
|
|
|
|
private _onPage(page: Page) {
|
|
// Annotate frame hierarchy so that snapshots could include frame ids.
|
|
for (const frame of page.frames())
|
|
this._annotateFrameHierarchy(frame);
|
|
this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame)));
|
|
}
|
|
|
|
private async _annotateFrameHierarchy(frame: Frame) {
|
|
try {
|
|
const frameElement = await frame.frameElement();
|
|
const parent = frame.parentFrame();
|
|
if (!parent)
|
|
return;
|
|
const context = await parent._mainContext();
|
|
await context?.evaluate(({ snapshotStreamer, frameElement, frameId }) => {
|
|
(window as any)[snapshotStreamer].markIframe(frameElement, frameId);
|
|
}, { snapshotStreamer: this._snapshotStreamer, frameElement, frameId: frame.guid });
|
|
frameElement.dispose();
|
|
} catch (e) {
|
|
}
|
|
}
|
|
}
|