angular-cn/packages/service-worker/worker/testing/mock.ts

235 lines
6.6 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AssetGroupConfig, Manifest} from '../src/manifest';
import {sha1} from '../src/sha1';
import {MockResponse} from './fetch';
export type HeaderMap = {
[key: string]: string
};
export class MockFile {
constructor(
readonly path: string, readonly contents: string, readonly headers = {},
readonly hashThisFile: boolean) {}
get hash(): string { return sha1(this.contents); }
}
export class MockFileSystemBuilder {
private resources = new Map<string, MockFile>();
addFile(path: string, contents: string, headers?: HeaderMap): MockFileSystemBuilder {
this.resources.set(path, new MockFile(path, contents, headers, true));
return this;
}
addUnhashedFile(path: string, contents: string, headers?: HeaderMap): MockFileSystemBuilder {
this.resources.set(path, new MockFile(path, contents, headers, false));
return this;
}
build(): MockFileSystem { return new MockFileSystem(this.resources); }
}
export class MockFileSystem {
constructor(private resources: Map<string, MockFile>) {}
lookup(path: string): MockFile|undefined { return this.resources.get(path); }
extend(): MockFileSystemBuilder {
const builder = new MockFileSystemBuilder();
Array.from(this.resources.keys()).forEach(path => {
const res = this.resources.get(path) !;
if (res.hashThisFile) {
builder.addFile(path, res.contents, res.headers);
} else {
builder.addUnhashedFile(path, res.contents, res.headers);
}
});
return builder;
}
list(): string[] { return Array.from(this.resources.keys()); }
}
export class MockServerStateBuilder {
private resources = new Map<string, Response>();
private errors = new Set<string>();
withStaticFiles(fs: MockFileSystem): MockServerStateBuilder {
fs.list().forEach(path => {
const file = fs.lookup(path) !;
this.resources.set(path, new MockResponse(file.contents, {headers: file.headers}));
});
return this;
}
withManifest(manifest: Manifest): MockServerStateBuilder {
this.resources.set('ngsw.json', new MockResponse(JSON.stringify(manifest)));
return this;
}
withRedirect(from: string, to: string, toContents: string): MockServerStateBuilder {
this.resources.set(from, new MockResponse(toContents, {redirected: true, url: to}));
this.resources.set(to, new MockResponse(toContents));
return this;
}
withError(url: string): MockServerStateBuilder {
this.errors.add(url);
return this;
}
build(): MockServerState {
// Take a "snapshot" of the current `resources` and `errors`.
const resources = new Map(this.resources.entries());
const errors = new Set(this.errors.values());
return new MockServerState(resources, errors);
}
}
export class MockServerState {
private requests: Request[] = [];
private gate: Promise<void> = Promise.resolve();
private resolve: Function|null = null;
// TODO(issue/24571): remove '!'.
private resolveNextRequest !: Function;
online = true;
nextRequest: Promise<Request>;
constructor(private resources: Map<string, Response>, private errors: Set<string>) {
this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; });
}
async fetch(req: Request): Promise<Response> {
this.resolveNextRequest(req);
this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; });
await this.gate;
if (!this.online) {
throw new Error('Offline.');
}
this.requests.push(req);
if ((req.credentials === 'include') || (req.mode === 'no-cors')) {
return new MockResponse(null, {status: 0, statusText: '', type: 'opaque'});
}
const url = req.url.split('?')[0];
if (this.resources.has(url)) {
return this.resources.get(url) !.clone();
}
if (this.errors.has(url)) {
throw new Error('Intentional failure!');
}
return new MockResponse(null, {status: 404, statusText: 'Not Found'});
}
pause(): void {
this.gate = new Promise(resolve => { this.resolve = resolve; });
}
unpause(): void {
if (this.resolve === null) {
return;
}
this.resolve();
this.resolve = null;
}
assertSawRequestFor(url: string): void {
if (!this.sawRequestFor(url)) {
throw new Error(`Expected request for ${url}, got none.`);
}
}
assertNoRequestFor(url: string): void {
if (this.sawRequestFor(url)) {
throw new Error(`Expected no request for ${url} but saw one.`);
}
}
sawRequestFor(url: string): boolean {
const matching = this.requests.filter(req => req.url.split('?')[0] === url);
if (matching.length > 0) {
this.requests = this.requests.filter(req => req !== matching[0]);
return true;
}
return false;
}
assertNoOtherRequests(): void {
if (!this.noOtherRequests()) {
throw new Error(
`Expected no other requests, got requests for ${this.requests.map(req => req.url.split('?')[0]).join(', ')}`);
}
}
noOtherRequests(): boolean { return this.requests.length === 0; }
clearRequests(): void { this.requests = []; }
reset(): void {
this.clearRequests();
this.nextRequest = new Promise(resolve => { this.resolveNextRequest = resolve; });
this.gate = Promise.resolve();
this.resolve = null;
this.online = true;
}
}
export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
const files = fs.list();
const hashTable: {[url: string]: string} = {};
files.forEach(path => { hashTable[path] = fs.lookup(path) !.hash; });
return {
configVersion: 1,
timestamp: 1234567890123,
index: '/index.html',
assetGroups: [
{
name: 'group',
installMode: 'prefetch',
updateMode: 'prefetch',
urls: files,
patterns: [],
},
],
navigationUrls: [], hashTable,
};
}
export function tmpHashTableForFs(
fs: MockFileSystem, breakHashes: {[url: string]: boolean} = {}): {[url: string]: string} {
const table: {[url: string]: string} = {};
fs.list().forEach(path => {
const file = fs.lookup(path) !;
if (file.hashThisFile) {
table[path] = file.hash;
if (breakHashes[path]) {
table[path] = table[path].split('').reverse().join('');
}
}
});
return table;
}
export function tmpHashTable(manifest: Manifest): Map<string, string> {
const map = new Map<string, string>();
Object.keys(manifest.hashTable).forEach(url => {
const hash = manifest.hashTable[url];
map.set(url, hash);
});
return map;
}