From 667aba7508c2772356ed252dff99018a7c07d861 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Mon, 6 Jul 2020 16:55:37 +0300 Subject: [PATCH] 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 --- .../service-worker/worker/test/happy_spec.ts | 240 +++++++++++------- .../service-worker/worker/testing/cache.ts | 21 +- .../service-worker/worker/testing/mock.ts | 53 +++- .../service-worker/worker/testing/scope.ts | 35 +-- .../service-worker/worker/testing/utils.ts | 43 ++++ 5 files changed, 258 insertions(+), 134 deletions(-) create mode 100644 packages/service-worker/worker/testing/utils.ts diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index 44b12a1880..9be5bf14c6 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -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(); }); diff --git a/packages/service-worker/worker/testing/cache.ts b/packages/service-worker/worker/testing/cache.ts index 3a9e430495..9f2d9824d7 100644 --- a/packages/service-worker/worker/testing/cache.ts +++ b/packages/service-worker/worker/testing/cache.ts @@ -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 { - 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 { - 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 { - 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(/[?#].*/, ''); diff --git a/packages/service-worker/worker/testing/mock.ts b/packages/service-worker/worker/testing/mock.ts index 179f0496aa..8b51e38ad4 100644 --- a/packages/service-worker/worker/testing/mock.ts +++ b/packages/service-worker/worker/testing/mock.ts @@ -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(); private errors = new Set(); - 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 { }); 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(/^\//, '')}`; +} diff --git a/packages/service-worker/worker/testing/scope.ts b/packages/service-worker/worker/testing/scope.ts index 74a8d5604b..44df03a2ed 100644 --- a/packages/service-worker/worker/testing/scope.ts +++ b/packages/service-worker/worker/testing/scope.ts @@ -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 { + fetch(req: RequestInfo): Promise { 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 { 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 {} diff --git a/packages/service-worker/worker/testing/utils.ts b/packages/service-worker/worker/testing/utils.ts new file mode 100644 index 0000000000..51be5b4a0d --- /dev/null +++ b/packages/service-worker/worker/testing/utils.ts @@ -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 || '', + }; +}