test(service-worker): make mock implementations more similar to actual ones (#37922)

This commit makes the mock implementations used is ServiceWorker tests
behave more similar to the actual ones.

PR Close #37922
This commit is contained in:
George Kalpakas 2020-07-06 16:55:37 +03:00 committed by atscott
parent d380e93b82
commit 667aba7508
5 changed files with 258 additions and 134 deletions

View File

@ -13,7 +13,7 @@ import {AssetGroupConfig, DataGroupConfig, Manifest} from '../src/manifest';
import {sha1} from '../src/sha1';
import {clearAllCaches, MockCache} from '../testing/cache';
import {MockRequest, MockResponse} from '../testing/fetch';
import {MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock';
import {MockFileSystem, MockFileSystemBuilder, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock';
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
(function() {
@ -352,7 +352,7 @@ describe('Driver', () => {
await scope.resolveSelfMessages();
scope.autoAdvanceTime = false;
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/ngsw.json');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertSawRequestFor('/redirected.txt');
@ -364,7 +364,7 @@ describe('Driver', () => {
it('initializes prefetched content correctly, after a request kicks it off', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/ngsw.json');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertSawRequestFor('/redirected.txt');
@ -381,7 +381,7 @@ describe('Driver', () => {
// Making a request initializes the driver (fetches assets).
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
expect(driver['latestHash']).toEqual(jasmine.any(String));
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/ngsw.json');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertSawRequestFor('/redirected.txt');
@ -400,7 +400,7 @@ describe('Driver', () => {
// Pushing a message initializes the driver (fetches assets).
await scope.handleMessage({action: 'foo'}, 'someClient');
expect(driver['latestHash']).toEqual(jasmine.any(String));
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/ngsw.json');
server.assertSawRequestFor('/foo.txt');
server.assertSawRequestFor('/bar.txt');
server.assertSawRequestFor('/redirected.txt');
@ -466,7 +466,7 @@ describe('Driver', () => {
scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true);
serverUpdate.assertSawRequestFor('ngsw.json');
serverUpdate.assertSawRequestFor('/ngsw.json');
serverUpdate.assertSawRequestFor('/foo.txt');
serverUpdate.assertSawRequestFor('/redirected.txt');
serverUpdate.assertNoOtherRequests();
@ -562,7 +562,7 @@ describe('Driver', () => {
scope.advance(12000);
await driver.idle.empty;
serverUpdate.assertSawRequestFor('ngsw.json');
serverUpdate.assertSawRequestFor('/ngsw.json');
serverUpdate.assertSawRequestFor('/foo.txt');
serverUpdate.assertSawRequestFor('/redirected.txt');
serverUpdate.assertNoOtherRequests();
@ -578,7 +578,7 @@ describe('Driver', () => {
scope.advance(12000);
await driver.idle.empty;
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/ngsw.json');
});
it('does not make concurrent checks for updates on navigation', async () => {
@ -593,7 +593,7 @@ describe('Driver', () => {
scope.advance(12000);
await driver.idle.empty;
server.assertSawRequestFor('ngsw.json');
server.assertSawRequestFor('/ngsw.json');
server.assertNoOtherRequests();
});
@ -791,70 +791,75 @@ describe('Driver', () => {
});
it('should bypass serviceworker on ngsw-bypass parameter', async () => {
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'true'}});
server.assertNoRequestFor('/foo.txt');
// NOTE:
// Requests that bypass the SW are not handled at all in the mock implementation of `scope`,
// therefore no requests reach the server.
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': 'anything'}});
server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/some/url', undefined, {headers: {'ngsw-bypass': 'true'}});
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypass': null!}});
server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/some/url', undefined, {headers: {'ngsw-bypass': 'anything'}});
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'NGSW-bypass': 'upperCASE'}});
server.assertNoRequestFor('/foo.txt');
await makeRequest(scope, '/some/url', undefined, {headers: {'ngsw-bypass': null!}});
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/foo.txt', undefined, {headers: {'ngsw-bypasss': 'anything'}});
server.assertSawRequestFor('/foo.txt');
await makeRequest(scope, '/some/url', undefined, {headers: {'NGSW-bypass': 'upperCASE'}});
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/some/url', undefined, {headers: {'ngsw-bypasss': 'anything'}});
server.assertSawRequestFor('/some/url');
server.clearRequests();
await makeRequest(scope, '/bar.txt?ngsw-bypass=true');
server.assertNoRequestFor('/bar.txt');
await makeRequest(scope, '/some/url?ngsw-bypass=true');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar.txt?ngsw-bypasss=true');
server.assertSawRequestFor('/bar.txt');
await makeRequest(scope, '/some/url?ngsw-bypasss=true');
server.assertSawRequestFor('/some/url');
server.clearRequests();
await makeRequest(scope, '/bar.txt?ngsw-bypaSS=something');
server.assertNoRequestFor('/bar.txt');
await makeRequest(scope, '/some/url?ngsw-bypaSS=something');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar.txt?testparam=test&ngsw-byPASS=anything');
server.assertNoRequestFor('/bar.txt');
await makeRequest(scope, '/some/url?testparam=test&ngsw-byPASS=anything');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar.txt?testparam=test&angsw-byPASS=anything');
server.assertSawRequestFor('/bar.txt');
await makeRequest(scope, '/some/url?testparam=test&angsw-byPASS=anything');
server.assertSawRequestFor('/some/url');
server.clearRequests();
await makeRequest(scope, '/bar&ngsw-bypass=true.txt?testparam=test&angsw-byPASS=anything');
server.assertSawRequestFor('/bar&ngsw-bypass=true.txt');
await makeRequest(scope, '/some/url&ngsw-bypass=true.txt?testparam=test&angsw-byPASS=anything');
server.assertSawRequestFor('/some/url&ngsw-bypass=true.txt');
server.clearRequests();
await makeRequest(scope, '/bar&ngsw-bypass=true.txt');
server.assertSawRequestFor('/bar&ngsw-bypass=true.txt');
await makeRequest(scope, '/some/url&ngsw-bypass=true.txt');
server.assertSawRequestFor('/some/url&ngsw-bypass=true.txt');
server.clearRequests();
await makeRequest(
scope, '/bar&ngsw-bypass=true.txt?testparam=test&ngSW-BYPASS=SOMETHING&testparam2=test');
server.assertNoRequestFor('/bar&ngsw-bypass=true.txt');
scope,
'/some/url&ngsw-bypass=true.txt?testparam=test&ngSW-BYPASS=SOMETHING&testparam2=test');
server.assertNoRequestFor('/some/url&ngsw-bypass=true.txt');
await makeRequest(scope, '/bar?testparam=test&ngsw-bypass');
server.assertNoRequestFor('/bar');
await makeRequest(scope, '/some/url?testparam=test&ngsw-bypass');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar?testparam=test&ngsw-bypass&testparam2');
server.assertNoRequestFor('/bar');
await makeRequest(scope, '/some/url?testparam=test&ngsw-bypass&testparam2');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar?ngsw-bypass&testparam2');
server.assertNoRequestFor('/bar');
await makeRequest(scope, '/some/url?ngsw-bypass&testparam2');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar?ngsw-bypass=&foo=ngsw-bypass');
server.assertNoRequestFor('/bar');
await makeRequest(scope, '/some/url?ngsw-bypass=&foo=ngsw-bypass');
server.assertNoRequestFor('/some/url');
await makeRequest(scope, '/bar?ngsw-byapass&testparam2');
server.assertSawRequestFor('/bar');
await makeRequest(scope, '/some/url?ngsw-byapass&testparam2');
server.assertSawRequestFor('/some/url');
});
it('unregisters when manifest 404s', async () => {
@ -922,115 +927,172 @@ describe('Driver', () => {
});
describe('cache naming', () => {
let uid: number;
// Helpers
const cacheKeysFor = (baseHref: string) =>
const cacheKeysFor = (baseHref: string, manifestHash: string) =>
[`ngsw:${baseHref}:db:control`,
`ngsw:${baseHref}:${manifestHash}:assets:assets:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:assets:meta`,
`ngsw:${baseHref}:${manifestHash}:assets:other:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:other:meta`,
`ngsw:${baseHref}:${manifestHash}:assets:lazy_prefetch:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:lazy_prefetch:meta`,
`ngsw:${baseHref}:${manifestHash}:assets:eager:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:eager:meta`,
`ngsw:${baseHref}:${manifestHash}:assets:lazy:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:${manifestHash}:assets:lazy:meta`,
`ngsw:${baseHref}:42:data:dynamic:api:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:42:data:dynamic:api:lru`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:42:data:dynamic:api:age`,
`ngsw:${baseHref}:43:data:dynamic:api-static:cache`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:lru`,
`ngsw:${baseHref}:db:ngsw:${baseHref}:43:data:dynamic:api-static:age`,
];
const createManifestWithBaseHref = (baseHref: string, distDir: MockFileSystem): Manifest => ({
configVersion: 1,
timestamp: 1234567890123,
index: `${baseHref}foo.txt`,
assetGroups: [
{
name: 'eager',
installMode: 'prefetch',
updateMode: 'prefetch',
urls: [
`${baseHref}foo.txt`,
`${baseHref}bar.txt`,
],
patterns: [],
cacheQueryOptions: {ignoreVary: true},
},
{
name: 'lazy',
installMode: 'lazy',
updateMode: 'lazy',
urls: [
`${baseHref}baz.txt`,
`${baseHref}qux.txt`,
],
patterns: [],
cacheQueryOptions: {ignoreVary: true},
},
],
dataGroups: [
{
name: 'api',
version: 42,
maxAge: 3600000,
maxSize: 100,
strategy: 'freshness',
patterns: [
'/api/.*',
],
cacheQueryOptions: {ignoreVary: true},
},
],
navigationUrls: processNavigationUrls(baseHref),
hashTable: tmpHashTableForFs(distDir, {}, baseHref),
});
const getClientAssignments = async (sw: SwTestHarness, baseHref: string) => {
const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache;
const dehydrated = cache.dehydrate();
return JSON.parse(dehydrated['/assignments'].body!);
};
const initializeSwFor =
async (baseHref: string, initialCacheState = '{}', serverState = server) => {
const initializeSwFor = async (baseHref: string, initialCacheState = '{}') => {
const newDistDir = dist.extend().addFile('/foo.txt', `this is foo v${++uid}`).build();
const newManifest = createManifestWithBaseHref(baseHref, newDistDir);
const newManifestHash = sha1(JSON.stringify(newManifest));
const serverState = new MockServerStateBuilder()
.withRootDirectory(baseHref)
.withStaticFiles(newDistDir)
.withManifest(newManifest)
.build();
const newScope = new SwTestHarnessBuilder(`http://localhost${baseHref}`)
.withCacheState(initialCacheState)
.withServerState(serverState)
.build();
const newDriver = new Driver(newScope, newScope, new CacheDatabase(newScope, newScope));
await makeRequest(newScope, '/foo.txt', baseHref.replace(/\//g, '_'));
await makeRequest(newScope, newManifest.index, baseHref.replace(/\//g, '_'));
await newDriver.initialized;
return newScope;
return [newScope, newManifestHash] as [SwTestHarness, string];
};
it('includes the SW scope in all cache names', async () => {
// Default SW with scope `/`.
await makeRequest(scope, '/foo.txt');
await driver.initialized;
const cacheNames = await scope.caches.keys();
beforeEach(() => {
uid = 0;
});
expect(cacheNames).toEqual(cacheKeysFor('/'));
it('includes the SW scope in all cache names', async () => {
// SW with scope `/`.
const [rootScope, rootManifestHash] = await initializeSwFor('/');
const cacheNames = await rootScope.caches.keys();
expect(cacheNames).toEqual(cacheKeysFor('/', rootManifestHash));
expect(cacheNames.every(name => name.includes('/'))).toBe(true);
// SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/');
const [fooScope, fooManifestHash] = await initializeSwFor('/foo/');
const fooCacheNames = await fooScope.caches.keys();
expect(fooCacheNames).toEqual(cacheKeysFor('/foo/'));
expect(fooCacheNames).toEqual(cacheKeysFor('/foo/', fooManifestHash));
expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true);
});
it('does not affect caches from other scopes', async () => {
// Create SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/');
const [fooScope, fooManifestHash] = await initializeSwFor('/foo/');
const fooAssignments = await getClientAssignments(fooScope, '/foo/');
expect(fooAssignments).toEqual({_foo_: manifestHash});
expect(fooAssignments).toEqual({_foo_: fooManifestHash});
// Add new SW with different scope.
const barScope = await initializeSwFor('/bar/', await fooScope.caches.dehydrate());
const [barScope, barManifestHash] =
await initializeSwFor('/bar/', await fooScope.caches.dehydrate());
const barCacheNames = await barScope.caches.keys();
const barAssignments = await getClientAssignments(barScope, '/bar/');
expect(barAssignments).toEqual({_bar_: manifestHash});
expect(barAssignments).toEqual({_bar_: barManifestHash});
expect(barCacheNames).toEqual([
...cacheKeysFor('/foo/'),
...cacheKeysFor('/bar/'),
...cacheKeysFor('/foo/', fooManifestHash),
...cacheKeysFor('/bar/', barManifestHash),
]);
// The caches for `/foo/` should be intact.
const fooAssignments2 = await getClientAssignments(barScope, '/foo/');
expect(fooAssignments2).toEqual({_foo_: manifestHash});
expect(fooAssignments2).toEqual({_foo_: fooManifestHash});
});
it('updates existing caches for same scope', async () => {
// Create SW with scope `/foo/`.
const fooScope = await initializeSwFor('/foo/');
await makeRequest(fooScope, '/foo.txt', '_bar_');
const [fooScope, fooManifestHash] = await initializeSwFor('/foo/');
await makeRequest(fooScope, '/foo/foo.txt', '_bar_');
const fooAssignments = await getClientAssignments(fooScope, '/foo/');
expect(fooAssignments).toEqual({
_foo_: manifestHash,
_bar_: manifestHash,
_foo_: fooManifestHash,
_bar_: fooManifestHash,
});
expect(await makeRequest(fooScope, '/baz.txt', '_foo_')).toBe('this is baz');
expect(await makeRequest(fooScope, '/baz.txt', '_bar_')).toBe('this is baz');
expect(await makeRequest(fooScope, '/foo/baz.txt', '_foo_')).toBe('this is baz');
expect(await makeRequest(fooScope, '/foo/baz.txt', '_bar_')).toBe('this is baz');
// Add new SW with same scope.
const fooScope2 =
await initializeSwFor('/foo/', await fooScope.caches.dehydrate(), serverUpdate);
const [fooScope2, fooManifestHash2] =
await initializeSwFor('/foo/', await fooScope.caches.dehydrate());
// Update client `_foo_` but not client `_bar_`.
await fooScope2.handleMessage({action: 'CHECK_FOR_UPDATES'}, '_foo_');
await fooScope2.handleMessage({action: 'ACTIVATE_UPDATE'}, '_foo_');
const fooAssignments2 = await getClientAssignments(fooScope2, '/foo/');
expect(fooAssignments2).toEqual({
_foo_: manifestUpdateHash,
_bar_: manifestHash,
_foo_: fooManifestHash2,
_bar_: fooManifestHash,
});
// Everything should still work as expected.
expect(await makeRequest(fooScope2, '/foo.txt', '_foo_')).toBe('this is foo v2');
expect(await makeRequest(fooScope2, '/foo.txt', '_bar_')).toBe('this is foo');
expect(await makeRequest(fooScope2, '/foo/foo.txt', '_foo_')).toBe('this is foo v2');
expect(await makeRequest(fooScope2, '/foo/foo.txt', '_bar_')).toBe('this is foo v1');
expect(await makeRequest(fooScope2, '/baz.txt', '_foo_')).toBe('this is baz v2');
expect(await makeRequest(fooScope2, '/baz.txt', '_bar_')).toBe('this is baz');
expect(await makeRequest(fooScope2, '/foo/baz.txt', '_foo_')).toBe('this is baz');
expect(await makeRequest(fooScope2, '/foo/baz.txt', '_bar_')).toBe('this is baz');
});
});
@ -1154,7 +1216,7 @@ describe('Driver', () => {
server.assertNoOtherRequests();
});
it('redirects to index on a request to the origin URL request', async () => {
it('redirects to index on a request to the scope URL', async () => {
expect(await navRequest('http://localhost/')).toEqual('this is foo');
server.assertNoOtherRequests();
});

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {MockRequest, MockResponse} from './fetch';
import {MockResponse} from './fetch';
import {normalizeUrl} from './utils';
export interface DehydratedResponse {
body: string|null;
@ -104,7 +105,7 @@ export class MockCache {
}
async 'delete'(request: RequestInfo, options?: CacheQueryOptions): Promise<boolean> {
let url = (typeof request === 'string' ? request : request.url);
let url = this.getRequestUrl(request);
if (this.cache.has(url)) {
this.cache.delete(url);
return true;
@ -127,10 +128,7 @@ export class MockCache {
}
async match(request: RequestInfo, options?: CacheQueryOptions): Promise<Response> {
let url = (typeof request === 'string' ? request : request.url);
if (url.startsWith(this.origin)) {
url = '/' + url.substr(this.origin.length);
}
let url = this.getRequestUrl(request);
// TODO: cleanup typings. Typescript doesn't know this can resolve to undefined.
let res = this.cache.get(url);
if (!res && options?.ignoreSearch) {
@ -150,8 +148,7 @@ export class MockCache {
if (request === undefined) {
return Array.from(this.cache.values());
}
const url = (typeof request === 'string' ? request : request.url);
const res = await this.match(url, options);
const res = await this.match(request, options);
if (res) {
return [res];
} else {
@ -160,7 +157,7 @@ export class MockCache {
}
async put(request: RequestInfo, response: Response): Promise<void> {
const url = (typeof request === 'string' ? request : request.url);
const url = this.getRequestUrl(request);
this.cache.set(url, response.clone());
// Even though the body above is cloned, consume it here because the
@ -190,6 +187,12 @@ export class MockCache {
return dehydrated;
}
/** Get the normalized URL from a `RequestInfo` value. */
private getRequestUrl(request: RequestInfo): string {
const url = typeof request === 'string' ? request : request.url;
return normalizeUrl(url, this.origin);
}
/** remove the query/hash part from a url*/
private stripQueryAndHash(url: string): string {
return url.replace(/[?#].*/, '');

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AssetGroupConfig, Manifest} from '../src/manifest';
import {Manifest} from '../src/manifest';
import {sha1} from '../src/sha1';
import {MockResponse} from './fetch';
@ -69,19 +69,38 @@ export class MockFileSystem {
}
export class MockServerStateBuilder {
private rootDir = '/';
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}));
withRootDirectory(newRootDir: string): MockServerStateBuilder {
// Update existing resources/errors.
const oldRootDir = this.rootDir;
const updateRootDir = (path: string) =>
path.startsWith(oldRootDir) ? joinPaths(newRootDir, path.slice(oldRootDir.length)) : path;
this.resources = new Map(
[...this.resources].map(([path, contents]) => [updateRootDir(path), contents.clone()]));
this.errors = new Set([...this.errors].map(url => updateRootDir(url)));
// Set `rootDir` for future resource/error additions.
this.rootDir = newRootDir;
return this;
}
withStaticFiles(dir: MockFileSystem): MockServerStateBuilder {
dir.list().forEach(path => {
const file = dir.lookup(path)!;
this.resources.set(
joinPaths(this.rootDir, path), new MockResponse(file.contents, {headers: file.headers}));
});
return this;
}
withManifest(manifest: Manifest): MockServerStateBuilder {
this.resources.set('ngsw.json', new MockResponse(JSON.stringify(manifest)));
const manifestPath = joinPaths(this.rootDir, 'ngsw.json');
this.resources.set(manifestPath, new MockResponse(JSON.stringify(manifest)));
return this;
}
@ -234,14 +253,16 @@ export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
}
export function tmpHashTableForFs(
fs: MockFileSystem, breakHashes: {[url: string]: boolean} = {}): {[url: string]: string} {
fs: MockFileSystem, breakHashes: {[url: string]: boolean} = {},
baseHref = '/'): {[url: string]: string} {
const table: {[url: string]: string} = {};
fs.list().forEach(path => {
const file = fs.lookup(path)!;
fs.list().forEach(filePath => {
const urlPath = joinPaths(baseHref, filePath);
const file = fs.lookup(filePath)!;
if (file.hashThisFile) {
table[path] = file.hash;
if (breakHashes[path]) {
table[path] = table[path].split('').reverse().join('');
table[urlPath] = file.hash;
if (breakHashes[filePath]) {
table[urlPath] = table[urlPath].split('').reverse().join('');
}
}
});
@ -256,3 +277,11 @@ export function tmpHashTable(manifest: Manifest): Map<string, string> {
});
return map;
}
// Helpers
/**
* Join two path segments, ensuring that there is exactly one slash (`/`) between them.
*/
function joinPaths(path1: string, path2: string): string {
return `${path1.replace(/\/$/, '')}/${path2.replace(/^\//, '')}`;
}

View File

@ -15,6 +15,7 @@ import {sha1} from '../src/sha1';
import {MockCacheStorage} from './cache';
import {MockHeaders, MockRequest, MockResponse} from './fetch';
import {MockServerState, MockServerStateBuilder} from './mock';
import {normalizeUrl, parseUrl} from './utils';
const EMPTY_SERVER_STATE = new MockServerStateBuilder().build();
@ -32,10 +33,11 @@ export class MockClient {
}
export class SwTestHarnessBuilder {
private origin = parseUrl(this.scopeUrl).origin;
private server = EMPTY_SERVER_STATE;
private caches = new MockCacheStorage(this.origin);
constructor(private origin = 'http://localhost/') {}
constructor(private scopeUrl = 'http://localhost/') {}
withCacheState(cache: string): SwTestHarnessBuilder {
this.caches = new MockCacheStorage(this.origin, cache);
@ -48,7 +50,7 @@ export class SwTestHarnessBuilder {
}
build(): SwTestHarness {
return new SwTestHarness(this.server, this.caches, this.origin);
return new SwTestHarness(this.server, this.caches, this.scopeUrl);
}
}
@ -137,6 +139,8 @@ export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope,
fired: boolean,
}[] = [];
parseUrl = parseUrl;
constructor(
private server: MockServerState, readonly caches: MockCacheStorage, scopeUrl: string) {
super(scopeUrl);
@ -176,17 +180,12 @@ export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope,
this.server = server || EMPTY_SERVER_STATE;
}
fetch(req: string|Request): Promise<Response> {
fetch(req: RequestInfo): Promise<Response> {
if (typeof req === 'string') {
if (req.startsWith(this.origin)) {
req = '/' + req.substr(this.origin.length);
}
return this.server.fetch(new MockRequest(req));
return this.server.fetch(new MockRequest(normalizeUrl(req, this.scopeUrl)));
} else {
const mockReq = req.clone() as MockRequest;
if (mockReq.url.startsWith(this.origin)) {
mockReq.url = '/' + mockReq.url.substr(this.origin.length);
}
mockReq.url = normalizeUrl(mockReq.url, this.scopeUrl);
return this.server.fetch(mockReq);
}
}
@ -200,7 +199,7 @@ export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope,
}
newRequest(url: string, init: Object = {}): Request {
return new MockRequest(url, init);
return new MockRequest(normalizeUrl(url, this.scopeUrl), init);
}
newResponse(body: string, init: Object = {}): Response {
@ -214,18 +213,6 @@ export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope,
}, new MockHeaders());
}
parseUrl(url: string, relativeTo?: string): {origin: string, path: string, search: string} {
const parsedUrl: URL = (typeof URL === 'function') ?
(!relativeTo ? new URL(url) : new URL(url, relativeTo)) :
require('url').parse(require('url').resolve(relativeTo || '', url));
return {
origin: parsedUrl.origin || `${parsedUrl.protocol}//${parsedUrl.host}`,
path: parsedUrl.pathname,
search: parsedUrl.search || '',
};
}
async skipWaiting(): Promise<void> {
this.skippedWaiting = true;
}
@ -409,6 +396,7 @@ class MockPushEvent extends MockExtendableEvent {
json: () => this._data,
};
}
class MockNotificationEvent extends MockExtendableEvent {
constructor(private _notification: any, readonly action?: string) {
super();
@ -418,5 +406,4 @@ class MockNotificationEvent extends MockExtendableEvent {
class MockInstallEvent extends MockExtendableEvent {}
class MockActivateEvent extends MockExtendableEvent {}

View File

@ -0,0 +1,43 @@
/**
* @license
* Copyright Google LLC 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
*/
/**
* Get a normalized representation of a URL relative to a provided base URL.
*
* More specifically:
* 1. Resolve the URL relative to the provided base URL.
* 2. If the URL is relative to the base URL, then strip the origin (and only return the path and
* search parts). Otherwise, return the full URL.
*
* @param url The raw URL.
* @param relativeTo The base URL to resolve `url` relative to.
* (This is usually the ServiceWorker's origin or registration scope).
* @return A normalized representation of the URL.
*/
export function normalizeUrl(url: string, relativeTo: string): string {
const {origin, path, search} = parseUrl(url, relativeTo);
const {origin: relativeToOrigin} = parseUrl(relativeTo);
return (origin === relativeToOrigin) ? path + search : url;
}
/**
* Parse a URL into its different parts, such as `origin`, `path` and `search`.
*/
export function parseUrl(
url: string, relativeTo?: string): {origin: string, path: string, search: string} {
const parsedUrl: URL = (typeof URL === 'function') ?
(!relativeTo ? new URL(url) : new URL(url, relativeTo)) :
require('url').parse(require('url').resolve(relativeTo || '', url));
return {
origin: parsedUrl.origin || `${parsedUrl.protocol}//${parsedUrl.host}`,
path: parsedUrl.pathname,
search: parsedUrl.search || '',
};
}