2021-01-08 01:15:34 +01:00
|
|
|
/**
|
|
|
|
|
* 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 * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import * as util from 'util';
|
2021-01-26 03:44:46 +01:00
|
|
|
import type { Frame, Route } from '../../..';
|
2021-01-25 23:49:51 +01:00
|
|
|
import { parsedURL } from '../../client/clientHelper';
|
2021-01-26 03:44:46 +01:00
|
|
|
import { ContextEntry, PageEntry, trace } from './traceModel';
|
2021-01-08 01:15:34 +01:00
|
|
|
|
|
|
|
|
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
|
|
|
|
|
|
|
|
|
export class SnapshotRouter {
|
|
|
|
|
private _contextEntry: ContextEntry | undefined;
|
|
|
|
|
private _unknownUrls = new Set<string>();
|
2021-01-26 03:44:46 +01:00
|
|
|
private _resourcesDir: string;
|
|
|
|
|
private _snapshotFrameIdToSnapshot = new Map<string, trace.FrameSnapshot>();
|
|
|
|
|
private _pageUrl = '';
|
|
|
|
|
private _frameToSnapshotFrameId = new Map<Frame, string>();
|
2021-01-08 01:15:34 +01:00
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
constructor(resourcesDir: string) {
|
|
|
|
|
this._resourcesDir = resourcesDir;
|
2021-01-08 01:15:34 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
// Returns the url to navigate to.
|
|
|
|
|
async selectSnapshot(contextEntry: ContextEntry, pageEntry: PageEntry, snapshotId?: string, timestamp?: number): Promise<string> {
|
2021-01-08 01:15:34 +01:00
|
|
|
this._contextEntry = contextEntry;
|
2021-01-26 03:44:46 +01:00
|
|
|
if (!snapshotId && !timestamp)
|
|
|
|
|
return 'data:text/html,Snapshot is not available';
|
|
|
|
|
|
|
|
|
|
const lastSnapshotEvent = new Map<string, trace.FrameSnapshotTraceEvent>();
|
|
|
|
|
for (const [frameId, snapshots] of pageEntry.snapshotsByFrameId) {
|
|
|
|
|
for (const snapshot of snapshots) {
|
|
|
|
|
const current = lastSnapshotEvent.get(frameId);
|
|
|
|
|
// Prefer snapshot with exact id.
|
|
|
|
|
const exactMatch = snapshotId && snapshot.snapshotId === snapshotId;
|
|
|
|
|
const currentExactMatch = current && snapshotId && current.snapshotId === snapshotId;
|
|
|
|
|
// If not available, prefer the latest snapshot before the timestamp.
|
|
|
|
|
const timestampMatch = timestamp && snapshot.timestamp <= timestamp;
|
|
|
|
|
if (exactMatch || (timestampMatch && !currentExactMatch))
|
|
|
|
|
lastSnapshotEvent.set(frameId, snapshot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._snapshotFrameIdToSnapshot.clear();
|
|
|
|
|
for (const [frameId, event] of lastSnapshotEvent) {
|
|
|
|
|
const buffer = await this._readSha1(event.sha1);
|
|
|
|
|
if (!buffer)
|
|
|
|
|
continue;
|
|
|
|
|
try {
|
|
|
|
|
const snapshot = JSON.parse(buffer.toString('utf8')) as trace.FrameSnapshot;
|
|
|
|
|
// Request url could come lower case, so we always normalize to lower case.
|
|
|
|
|
this._snapshotFrameIdToSnapshot.set(frameId.toLowerCase(), snapshot);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mainFrameSnapshot = lastSnapshotEvent.get('');
|
|
|
|
|
if (!mainFrameSnapshot)
|
|
|
|
|
return 'data:text/html,Snapshot is not available';
|
|
|
|
|
|
|
|
|
|
if (!mainFrameSnapshot.frameUrl.startsWith('http'))
|
|
|
|
|
this._pageUrl = 'http://playwright.snapshot/';
|
|
|
|
|
else
|
|
|
|
|
this._pageUrl = mainFrameSnapshot.frameUrl;
|
|
|
|
|
return this._pageUrl;
|
2021-01-08 01:15:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async route(route: Route) {
|
|
|
|
|
const url = route.request().url();
|
2021-01-26 03:44:46 +01:00
|
|
|
const frame = route.request().frame();
|
|
|
|
|
|
|
|
|
|
if (route.request().isNavigationRequest()) {
|
|
|
|
|
let snapshotFrameId: string | undefined;
|
|
|
|
|
if (url === this._pageUrl) {
|
|
|
|
|
snapshotFrameId = '';
|
|
|
|
|
} else {
|
|
|
|
|
snapshotFrameId = url.substring(url.indexOf('://') + 3);
|
|
|
|
|
if (snapshotFrameId.endsWith('/'))
|
|
|
|
|
snapshotFrameId = snapshotFrameId.substring(0, snapshotFrameId.length - 1);
|
|
|
|
|
// Request url could come lower case, so we always normalize to lower case.
|
|
|
|
|
snapshotFrameId = snapshotFrameId.toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const snapshot = this._snapshotFrameIdToSnapshot.get(snapshotFrameId);
|
|
|
|
|
if (!snapshot) {
|
|
|
|
|
route.fulfill({
|
|
|
|
|
contentType: 'text/html',
|
|
|
|
|
body: 'data:text/html,Snapshot is not available',
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._frameToSnapshotFrameId.set(frame, snapshotFrameId);
|
2021-01-08 01:15:34 +01:00
|
|
|
route.fulfill({
|
|
|
|
|
contentType: 'text/html',
|
2021-01-26 03:44:46 +01:00
|
|
|
body: snapshot.html,
|
2021-01-08 01:15:34 +01:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
const snapshotFrameId = this._frameToSnapshotFrameId.get(frame);
|
|
|
|
|
if (snapshotFrameId === undefined)
|
|
|
|
|
return this._routeUnknown(route);
|
|
|
|
|
const snapshot = this._snapshotFrameIdToSnapshot.get(snapshotFrameId);
|
|
|
|
|
if (!snapshot)
|
2021-01-08 01:15:34 +01:00
|
|
|
return this._routeUnknown(route);
|
|
|
|
|
|
|
|
|
|
// Find a matching resource from the same context, preferrably from the same frame.
|
|
|
|
|
// Note: resources are stored without hash, but page may reference them with hash.
|
2021-01-26 03:44:46 +01:00
|
|
|
let resource: trace.NetworkResourceTraceEvent | null = null;
|
2021-01-08 01:15:34 +01:00
|
|
|
const resourcesWithUrl = this._contextEntry!.resourcesByUrl.get(removeHash(url)) || [];
|
|
|
|
|
for (const resourceEvent of resourcesWithUrl) {
|
2021-01-26 03:44:46 +01:00
|
|
|
if (resource && resourceEvent.frameId !== snapshotFrameId)
|
2021-01-08 01:15:34 +01:00
|
|
|
continue;
|
|
|
|
|
resource = resourceEvent;
|
2021-01-26 03:44:46 +01:00
|
|
|
if (resourceEvent.frameId === snapshotFrameId)
|
2021-01-08 01:15:34 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!resource)
|
|
|
|
|
return this._routeUnknown(route);
|
|
|
|
|
|
|
|
|
|
// This particular frame might have a resource content override, for example when
|
|
|
|
|
// stylesheet is modified using CSSOM.
|
2021-01-26 03:44:46 +01:00
|
|
|
const resourceOverride = snapshot.resourceOverrides.find(o => o.url === url);
|
2021-01-08 01:15:34 +01:00
|
|
|
const overrideSha1 = resourceOverride ? resourceOverride.sha1 : undefined;
|
|
|
|
|
const resourceData = await this._readResource(resource, overrideSha1);
|
|
|
|
|
if (!resourceData)
|
|
|
|
|
return this._routeUnknown(route);
|
|
|
|
|
const headers: { [key: string]: string } = {};
|
|
|
|
|
for (const { name, value } of resourceData.headers)
|
|
|
|
|
headers[name] = value;
|
|
|
|
|
headers['Access-Control-Allow-Origin'] = '*';
|
|
|
|
|
route.fulfill({
|
|
|
|
|
contentType: resourceData.contentType,
|
|
|
|
|
body: resourceData.body,
|
|
|
|
|
headers,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _routeUnknown(route: Route) {
|
|
|
|
|
const url = route.request().url();
|
|
|
|
|
if (!this._unknownUrls.has(url)) {
|
|
|
|
|
console.log(`Request to unknown url: ${url}`); /* eslint-disable-line no-console */
|
|
|
|
|
this._unknownUrls.add(url);
|
|
|
|
|
}
|
|
|
|
|
route.abort();
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-26 03:44:46 +01:00
|
|
|
private async _readSha1(sha1: string) {
|
2021-01-08 01:15:34 +01:00
|
|
|
try {
|
2021-01-26 03:44:46 +01:00
|
|
|
return await fsReadFileAsync(path.join(this._resourcesDir, sha1));
|
2021-01-08 01:15:34 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-01-26 03:44:46 +01:00
|
|
|
|
|
|
|
|
private async _readResource(event: trace.NetworkResourceTraceEvent, overrideSha1: string | undefined) {
|
2021-01-26 20:06:05 +01:00
|
|
|
const body = await this._readSha1(overrideSha1 || event.responseSha1);
|
2021-01-26 03:44:46 +01:00
|
|
|
if (!body)
|
|
|
|
|
return;
|
|
|
|
|
return {
|
|
|
|
|
contentType: event.contentType,
|
|
|
|
|
body,
|
|
|
|
|
headers: event.responseHeaders,
|
|
|
|
|
};
|
|
|
|
|
}
|
2021-01-08 01:15:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeHash(url: string) {
|
2021-01-25 23:49:51 +01:00
|
|
|
const u = parsedURL(url);
|
|
|
|
|
if (!u)
|
2021-01-08 01:15:34 +01:00
|
|
|
return url;
|
2021-01-25 23:49:51 +01:00
|
|
|
u.hash = '';
|
|
|
|
|
return u.toString();
|
2021-01-08 01:15:34 +01:00
|
|
|
}
|