playwright/src/cli/traceViewer/snapshotRouter.ts
2021-01-07 16:15:34 -08:00

123 lines
4.1 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 * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import type { Route } from '../../..';
import type { FrameSnapshot, NetworkResourceTraceEvent, PageSnapshot } from '../../trace/traceTypes';
import { ContextEntry } from './traceModel';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
export class SnapshotRouter {
private _contextEntry: ContextEntry | undefined;
private _unknownUrls = new Set<string>();
private _traceStorageDir: string;
private _frameBySrc = new Map<string, FrameSnapshot>();
constructor(traceStorageDir: string) {
this._traceStorageDir = traceStorageDir;
}
selectSnapshot(snapshot: PageSnapshot, contextEntry: ContextEntry) {
this._frameBySrc.clear();
this._contextEntry = contextEntry;
for (const frameSnapshot of snapshot.frames)
this._frameBySrc.set(frameSnapshot.url, frameSnapshot);
}
async route(route: Route) {
const url = route.request().url();
if (this._frameBySrc.has(url)) {
const frameSnapshot = this._frameBySrc.get(url)!;
route.fulfill({
contentType: 'text/html',
body: Buffer.from(frameSnapshot.html),
});
return;
}
const frameSrc = route.request().frame().url();
const frameSnapshot = this._frameBySrc.get(frameSrc);
if (!frameSnapshot)
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.
let resource: NetworkResourceTraceEvent | null = null;
const resourcesWithUrl = this._contextEntry!.resourcesByUrl.get(removeHash(url)) || [];
for (const resourceEvent of resourcesWithUrl) {
if (resource && resourceEvent.frameId !== frameSnapshot.frameId)
continue;
resource = resourceEvent;
if (resourceEvent.frameId === frameSnapshot.frameId)
break;
}
if (!resource)
return this._routeUnknown(route);
// This particular frame might have a resource content override, for example when
// stylesheet is modified using CSSOM.
const resourceOverride = frameSnapshot.resourceOverrides.find(o => o.url === url);
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();
}
private async _readResource(event: NetworkResourceTraceEvent, overrideSha1: string | undefined) {
try {
const body = await fsReadFileAsync(path.join(this._traceStorageDir, overrideSha1 || event.sha1));
return {
contentType: event.contentType,
body,
headers: event.responseHeaders,
};
} catch (e) {
return undefined;
}
}
}
function removeHash(url: string) {
try {
const u = new URL(url);
u.hash = '';
return u.toString();
} catch (e) {
return url;
}
}