feat(service-worker): add support for configuring navigations URLs (#23339)
The ServiceWorker will redirect navigation requests that don't match any `asset` or `data` group to the specified index file. The rules for a request to be classified as a navigation request are as follows: 1. Its `mode` must be `navigation`. 2. It must accept a `text/html` response. 3. Its URL must match certain criteria (see below). By default, a navigation request can have any URL except for: 1. URLs containing `__`. 2. URLs to files (i.e. containing a file extension in the last path segment). While these rules are fine in many cases, sometimes it is desirable to configure different rules for the URLs of navigation requests (e.g. ignore specific URLs and pass them through to the server). This commit adds support for specifying an optional `navigationUrls` list in `ngsw-config.json`, which contains URLs or simple globs (currently only recognizing `!`, `*` and `**`). Only requests whose URLs match any of the positive URLs/patterns and none of the negative ones (i.e. URLs/patterns starting with `!`) will be considered navigation requests (and handled accordingly by the SW). (This is an alternative implementation to #23025.) Fixes #20404 PR Close #23339
This commit is contained in:

committed by
Igor Minar

parent
1e1c7fd408
commit
08325aaffc
@ -11,6 +11,13 @@ import {Filesystem} from './filesystem';
|
||||
import {globToRegex} from './glob';
|
||||
import {Config} from './in';
|
||||
|
||||
const DEFAULT_NAVIGATION_URLS = [
|
||||
'/**', // Include all URLs.
|
||||
'!/**/*.*', // Exclude URLs to files (containing a file extension in the last segment).
|
||||
'!/**/*__*', // Exclude URLs containing `__` in the last segment.
|
||||
'!/**/*__*/**', // Exclude URLs containing `__` in any other segment.
|
||||
];
|
||||
|
||||
/**
|
||||
* Consumes service worker configuration files and processes them into control files.
|
||||
*
|
||||
@ -23,10 +30,11 @@ export class Generator {
|
||||
const hashTable = {};
|
||||
return {
|
||||
configVersion: 1,
|
||||
index: joinUrls(this.baseHref, config.index),
|
||||
appData: config.appData,
|
||||
index: joinUrls(this.baseHref, config.index),
|
||||
assetGroups: await this.processAssetGroups(config, hashTable),
|
||||
dataGroups: this.processDataGroups(config), hashTable,
|
||||
navigationUrls: processNavigationUrls(this.baseHref, config.navigationUrls),
|
||||
};
|
||||
}
|
||||
|
||||
@ -80,6 +88,15 @@ export class Generator {
|
||||
}
|
||||
}
|
||||
|
||||
export function processNavigationUrls(
|
||||
baseHref: string, urls = DEFAULT_NAVIGATION_URLS): {positive: boolean, regex: string}[] {
|
||||
return urls.map(url => {
|
||||
const positive = !url.startsWith('!');
|
||||
url = positive ? url : url.substr(1);
|
||||
return {positive, regex: `^${urlToRegex(url, baseHref)}$`};
|
||||
});
|
||||
}
|
||||
|
||||
function globListToMatcher(globs: string[]): (file: string) => boolean {
|
||||
const patterns = globs.map(pattern => {
|
||||
if (pattern.startsWith('!')) {
|
||||
|
@ -26,6 +26,7 @@ export interface Config {
|
||||
index: string;
|
||||
assetGroups?: AssetGroup[];
|
||||
dataGroups?: DataGroup[];
|
||||
navigationUrls?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,4 +53,4 @@ export interface DataGroup {
|
||||
cacheConfig: {
|
||||
maxSize: number; maxAge: Duration; timeout?: Duration; strategy?: 'freshness' | 'performance';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,10 @@ import {MockFilesystem} from '../testing/mock';
|
||||
});
|
||||
const gen = new Generator(fs, '/test');
|
||||
const res = gen.process({
|
||||
index: '/index.html',
|
||||
appData: {
|
||||
test: true,
|
||||
},
|
||||
index: '/index.html',
|
||||
assetGroups: [{
|
||||
name: 'test',
|
||||
resources: {
|
||||
@ -52,40 +52,56 @@ import {MockFilesystem} from '../testing/mock';
|
||||
maxAge: '3d',
|
||||
timeout: '1m',
|
||||
}
|
||||
}]
|
||||
}],
|
||||
navigationUrls: [
|
||||
'/included/absolute/**',
|
||||
'!/excluded/absolute/**',
|
||||
'/included/some/url?with+escaped+chars',
|
||||
'!excluded/relative/*.txt',
|
||||
'http://example.com/included',
|
||||
'!http://example.com/excluded',
|
||||
],
|
||||
});
|
||||
res.then(config => {
|
||||
expect(config).toEqual({
|
||||
'configVersion': 1,
|
||||
'index': '/test/index.html',
|
||||
'appData': {
|
||||
'test': true,
|
||||
configVersion: 1,
|
||||
appData: {
|
||||
test: true,
|
||||
},
|
||||
'assetGroups': [{
|
||||
'name': 'test',
|
||||
'installMode': 'prefetch',
|
||||
'updateMode': 'prefetch',
|
||||
'urls': [
|
||||
index: '/test/index.html',
|
||||
assetGroups: [{
|
||||
name: 'test',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: [
|
||||
'/test/index.html',
|
||||
'/test/foo/test.html',
|
||||
'/test/test.txt',
|
||||
],
|
||||
'patterns': [
|
||||
patterns: [
|
||||
'\\/absolute\\/.*',
|
||||
'\\/some\\/url\\?with\\+escaped\\+chars',
|
||||
'\\/test\\/relative\\/[^\\/]+\\.txt',
|
||||
]
|
||||
}],
|
||||
'dataGroups': [{
|
||||
'name': 'other',
|
||||
'patterns': ['\\/api\\/.*', '\\/test\\/relapi\\/.*'],
|
||||
'strategy': 'performance',
|
||||
'maxSize': 100,
|
||||
'maxAge': 259200000,
|
||||
'timeoutMs': 60000,
|
||||
'version': 1,
|
||||
dataGroups: [{
|
||||
name: 'other',
|
||||
patterns: ['\\/api\\/.*', '\\/test\\/relapi\\/.*'],
|
||||
strategy: 'performance',
|
||||
maxSize: 100,
|
||||
maxAge: 259200000,
|
||||
timeoutMs: 60000,
|
||||
version: 1,
|
||||
}],
|
||||
'hashTable': {
|
||||
navigationUrls: [
|
||||
{positive: true, regex: '^\\/included\\/absolute\\/.*$'},
|
||||
{positive: false, regex: '^\\/excluded\\/absolute\\/.*$'},
|
||||
{positive: true, regex: '^\\/included\\/some\\/url\\?with\\+escaped\\+chars$'},
|
||||
{positive: false, regex: '^\\/test\\/excluded\\/relative\\/[^\\/]+\\.txt$'},
|
||||
{positive: true, regex: '^http:\\/\\/example\\.com\\/included$'},
|
||||
{positive: false, regex: '^http:\\/\\/example\\.com\\/excluded$'},
|
||||
],
|
||||
hashTable: {
|
||||
'/test/test.txt': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643',
|
||||
'/test/index.html': 'a54d88e06612d820bc3be72877c74f257b561b19',
|
||||
'/test/foo/test.html': '18f6f8eb7b1c23d2bb61bff028b83d867a9e4643'
|
||||
@ -95,5 +111,32 @@ import {MockFilesystem} from '../testing/mock';
|
||||
})
|
||||
.catch(err => done.fail(err));
|
||||
});
|
||||
|
||||
it('uses default `navigationUrls` if not provided', (done: DoneFn) => {
|
||||
const fs = new MockFilesystem({
|
||||
'/index.html': 'This is a test',
|
||||
});
|
||||
const gen = new Generator(fs, '/test');
|
||||
const res = gen.process({
|
||||
index: '/index.html',
|
||||
});
|
||||
res.then(config => {
|
||||
expect(config).toEqual({
|
||||
configVersion: 1,
|
||||
appData: undefined,
|
||||
index: '/test/index.html',
|
||||
assetGroups: [],
|
||||
dataGroups: [],
|
||||
navigationUrls: [
|
||||
{positive: true, regex: '^\\/.*$'},
|
||||
{positive: false, regex: '^\\/(?:.+\\/)?[^\\/]+\\.[^\\/]+$'},
|
||||
{positive: false, regex: '^\\/(?:.+\\/)?[^\\/]+__[^\\/]+\\/.*$'},
|
||||
],
|
||||
hashTable: {}
|
||||
});
|
||||
done();
|
||||
})
|
||||
.catch(err => done.fail(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ const manifest: Manifest = {
|
||||
urls: ['/only.txt'],
|
||||
patterns: [],
|
||||
}],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
|
||||
@ -55,6 +56,7 @@ const manifestUpdate: Manifest = {
|
||||
urls: ['/only.txt'],
|
||||
patterns: [],
|
||||
}],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(distUpdate),
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,6 @@ import {DataGroup} from './data';
|
||||
import {Database} from './database';
|
||||
import {IdleScheduler} from './idle';
|
||||
import {Manifest} from './manifest';
|
||||
import {isNavigationRequest} from './util';
|
||||
|
||||
|
||||
/**
|
||||
@ -40,6 +39,12 @@ export class AppVersion implements UpdateSource {
|
||||
*/
|
||||
private dataGroups: DataGroup[];
|
||||
|
||||
/**
|
||||
* Requests to URLs that match any of the `include` RegExps and none of the `exclude` RegExps
|
||||
* are considered navigation requests and handled accordingly.
|
||||
*/
|
||||
private navigationUrls: {include: RegExp[], exclude: RegExp[]};
|
||||
|
||||
/**
|
||||
* Tracks whether the manifest has encountered any inconsistencies.
|
||||
*/
|
||||
@ -79,6 +84,14 @@ export class AppVersion implements UpdateSource {
|
||||
config => new DataGroup(
|
||||
this.scope, this.adapter, config, this.database,
|
||||
`ngsw:${config.version}:data`));
|
||||
|
||||
// Create `include`/`exclude` RegExps for the `navigationUrls` declared in the manifest.
|
||||
const includeUrls = manifest.navigationUrls.filter(spec => spec.positive);
|
||||
const excludeUrls = manifest.navigationUrls.filter(spec => !spec.positive);
|
||||
this.navigationUrls = {
|
||||
include: includeUrls.map(spec => new RegExp(spec.regex)),
|
||||
exclude: excludeUrls.map(spec => new RegExp(spec.regex)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,15 +164,36 @@ export class AppVersion implements UpdateSource {
|
||||
|
||||
// Next, check if this is a navigation request for a route. Detect circular
|
||||
// navigations by checking if the request URL is the same as the index URL.
|
||||
if (isNavigationRequest(req, this.scope.registration.scope, this.adapter) &&
|
||||
req.url !== this.manifest.index) {
|
||||
if (req.url !== this.manifest.index && this.isNavigationRequest(req)) {
|
||||
// This was a navigation request. Re-enter `handleFetch` with a request for
|
||||
// the URL.
|
||||
return this.handleFetch(this.adapter.newRequest(this.manifest.index), context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the request is a navigation request.
|
||||
* Takes into account: Request mode, `Accept` header, `navigationUrls` patterns.
|
||||
*/
|
||||
isNavigationRequest(req: Request): boolean {
|
||||
if (req.mode !== 'navigate') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.acceptsTextHtml(req)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const urlPrefix = this.scope.registration.scope.replace(/\/$/, '');
|
||||
const url = req.url.startsWith(urlPrefix) ? req.url.substr(urlPrefix.length) : req.url;
|
||||
const urlWithoutQueryOrHash = url.replace(/[?#].*$/, '');
|
||||
|
||||
return this.navigationUrls.include.some(regex => regex.test(urlWithoutQueryOrHash)) &&
|
||||
!this.navigationUrls.exclude.some(regex => regex.test(urlWithoutQueryOrHash));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check this version for a given resource with a particular hash.
|
||||
*/
|
||||
@ -239,4 +273,16 @@ export class AppVersion implements UpdateSource {
|
||||
* Get the opaque application data which was provided with the manifest.
|
||||
*/
|
||||
get appData(): Object|null { return this.manifest.appData || null; }
|
||||
|
||||
/**
|
||||
* Check whether a request accepts `text/html` (based on the `Accept` header).
|
||||
*/
|
||||
private acceptsTextHtml(req: Request): boolean {
|
||||
const accept = req.headers.get('Accept');
|
||||
if (accept === null) {
|
||||
return false;
|
||||
}
|
||||
const values = accept.split(',');
|
||||
return values.some(value => value.trim().toLowerCase() === 'text/html');
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import {SwCriticalError} from './error';
|
||||
import {IdleScheduler} from './idle';
|
||||
import {Manifest, ManifestHash, hashManifest} from './manifest';
|
||||
import {MsgAny, isMsgActivateUpdate, isMsgCheckForUpdates} from './msg';
|
||||
import {isNavigationRequest} from './util';
|
||||
|
||||
type ClientId = string;
|
||||
|
||||
@ -551,13 +550,14 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
// Check if there is an assigned client id.
|
||||
if (this.clientVersionMap.has(clientId)) {
|
||||
// There is an assignment for this client already.
|
||||
let hash = this.clientVersionMap.get(clientId) !;
|
||||
const hash = this.clientVersionMap.get(clientId) !;
|
||||
let appVersion = this.lookupVersionByHash(hash, 'assignVersion');
|
||||
|
||||
// Ordinarily, this client would be served from its assigned version. But, if this
|
||||
// request is a navigation request, this client can be updated to the latest
|
||||
// version immediately.
|
||||
if (this.state === DriverReadyState.NORMAL && hash !== this.latestHash &&
|
||||
isNavigationRequest(event.request, this.scope.registration.scope, this.adapter)) {
|
||||
appVersion.isNavigationRequest(event.request)) {
|
||||
// Update this client to the latest version immediately.
|
||||
if (this.latestHash === null) {
|
||||
throw new Error(`Invariant violated (assignVersion): latestHash was null`);
|
||||
@ -566,11 +566,11 @@ export class Driver implements Debuggable, UpdateSource {
|
||||
const client = await this.scope.clients.get(clientId);
|
||||
|
||||
await this.updateClient(client);
|
||||
hash = this.latestHash;
|
||||
appVersion = this.lookupVersionByHash(this.latestHash, 'assignVersion');
|
||||
}
|
||||
|
||||
// TODO: make sure the version is valid.
|
||||
return this.lookupVersionByHash(hash, 'assignVersion');
|
||||
return appVersion;
|
||||
} else {
|
||||
// This is the first time this client ID has been seen. Whether the SW is in a
|
||||
// state to handle new clients depends on the current readiness state, so check
|
||||
|
@ -16,6 +16,7 @@ export interface Manifest {
|
||||
index: string;
|
||||
assetGroups?: AssetGroupConfig[];
|
||||
dataGroups?: DataGroupConfig[];
|
||||
navigationUrls: {positive: boolean, regex: string}[];
|
||||
hashTable: {[url: string]: string};
|
||||
}
|
||||
|
||||
@ -40,4 +41,4 @@ export interface DataGroupConfig {
|
||||
|
||||
export function hashManifest(manifest: Manifest): ManifestHash {
|
||||
return sha1(JSON.stringify(manifest));
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
/**
|
||||
* @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 {Adapter} from './adapter';
|
||||
|
||||
export function isNavigationRequest(req: Request, relativeTo: string, adapter: Adapter): boolean {
|
||||
if (req.mode !== 'navigate') {
|
||||
return false;
|
||||
}
|
||||
if (req.url.indexOf('__') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (hasFileExtension(req.url, relativeTo, adapter)) {
|
||||
return false;
|
||||
}
|
||||
if (!acceptsTextHtml(req)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasFileExtension(url: string, relativeTo: string, adapter: Adapter): boolean {
|
||||
const path = adapter.parseUrl(url, relativeTo).path;
|
||||
const lastSegment = path.split('/').pop() !;
|
||||
return lastSegment.indexOf('.') !== -1;
|
||||
}
|
||||
|
||||
function acceptsTextHtml(req: Request): boolean {
|
||||
const accept = req.headers.get('Accept');
|
||||
if (accept === null) {
|
||||
return false;
|
||||
}
|
||||
const values = accept.split(',');
|
||||
return values.some(value => value.trim().toLowerCase() === 'text/html');
|
||||
}
|
@ -9,6 +9,7 @@ ts_library(
|
||||
),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/service-worker/config",
|
||||
"//packages/service-worker/worker",
|
||||
"//packages/service-worker/worker/testing",
|
||||
],
|
||||
|
@ -82,6 +82,7 @@ const manifest: Manifest = {
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
navigationUrls: [],
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
|
||||
@ -268,4 +269,4 @@ function makePendingRequest(scope: SwTestHarness, url: string, clientId?: string
|
||||
})(),
|
||||
done
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {processNavigationUrls} from '../../config/src/generator';
|
||||
import {CacheDatabase} from '../src/db-cache';
|
||||
import {Driver, DriverReadyState} from '../src/driver';
|
||||
import {Manifest} from '../src/manifest';
|
||||
@ -34,6 +35,8 @@ const distUpdate =
|
||||
.addFile('/qux.txt', 'this is qux v2')
|
||||
.addFile('/quux.txt', 'this is quux v2')
|
||||
.addUnhashedFile('/unhashed/a.txt', 'this is unhashed v2', {'Cache-Control': 'max-age=10'})
|
||||
.addUnhashedFile('/ignored/file1', 'this is not handled by the SW')
|
||||
.addUnhashedFile('/ignored/dir/file2', 'this is not handled by the SW either')
|
||||
.build();
|
||||
|
||||
const brokenFs = new MockFileSystemBuilder().addFile('/foo.txt', 'this is foo').build();
|
||||
@ -51,6 +54,7 @@ const brokenManifest: Manifest = {
|
||||
patterns: [],
|
||||
}],
|
||||
dataGroups: [],
|
||||
navigationUrls: processNavigationUrls(''),
|
||||
hashTable: tmpHashTableForFs(brokenFs, {'/foo.txt': true}),
|
||||
};
|
||||
|
||||
@ -92,6 +96,7 @@ const manifest: Manifest = {
|
||||
patterns: [],
|
||||
}
|
||||
],
|
||||
navigationUrls: processNavigationUrls(''),
|
||||
hashTable: tmpHashTableForFs(dist),
|
||||
};
|
||||
|
||||
@ -133,6 +138,14 @@ const manifestUpdate: Manifest = {
|
||||
patterns: [],
|
||||
}
|
||||
],
|
||||
navigationUrls: processNavigationUrls(
|
||||
'',
|
||||
[
|
||||
'/**/file1',
|
||||
'/**/file2',
|
||||
'!/ignored/file1',
|
||||
'!/ignored/dir/**',
|
||||
]),
|
||||
hashTable: tmpHashTableForFs(distUpdate),
|
||||
};
|
||||
|
||||
@ -394,7 +407,7 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
|
||||
expect(await driver.checkForUpdate()).toEqual(true);
|
||||
serverUpdate.clearRequests();
|
||||
|
||||
expect(await makeRequest(scope, '/baz', 'default', {
|
||||
expect(await makeRequest(scope, '/file1', 'default', {
|
||||
headers: {
|
||||
'Accept': 'text/plain, text/html, text/css',
|
||||
},
|
||||
@ -626,6 +639,11 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
|
||||
});
|
||||
|
||||
describe('routing', () => {
|
||||
const navRequest = (url: string, init = {}) => makeRequest(scope, url, undefined, {
|
||||
headers: {Accept: 'text/plain, text/html, text/css'},
|
||||
mode: 'navigate', ...init,
|
||||
});
|
||||
|
||||
async_beforeEach(async() => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
@ -633,52 +651,95 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
|
||||
});
|
||||
|
||||
async_it('redirects to index on a route-like request', async() => {
|
||||
expect(await makeRequest(scope, '/baz', 'default', {
|
||||
headers: {
|
||||
'Accept': 'text/plain, text/html, text/css',
|
||||
},
|
||||
mode: 'navigate',
|
||||
})).toEqual('this is foo');
|
||||
expect(await navRequest('/baz')).toEqual('this is foo');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
async_it('redirects to index on a request to the origin URL request', async() => {
|
||||
expect(await makeRequest(scope, 'http://example.com', 'default', {
|
||||
headers: {
|
||||
'Accept': 'text/plain, text/html, text/css',
|
||||
},
|
||||
mode: 'navigate',
|
||||
})).toEqual('this is foo');
|
||||
expect(await navRequest('http://localhost/')).toEqual('this is foo');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
async_it('does not redirect to index on a non-navigation request', async() => {
|
||||
expect(await makeRequest(scope, '/baz', 'default', {
|
||||
headers: {
|
||||
'Accept': 'text/plain, text/html, text/css',
|
||||
},
|
||||
})).toBeNull();
|
||||
expect(await navRequest('/baz', {mode: undefined})).toBeNull();
|
||||
server.assertSawRequestFor('/baz');
|
||||
});
|
||||
|
||||
async_it('does not redirect to index on a request with an extension', async() => {
|
||||
expect(await makeRequest(scope, '/baz.html', 'default', {
|
||||
headers: {
|
||||
'Accept': 'text/plain, text/html, text/css',
|
||||
},
|
||||
mode: 'navigate',
|
||||
})).toBeNull();
|
||||
server.assertSawRequestFor('/baz.html');
|
||||
});
|
||||
|
||||
async_it('does not redirect to index on a request that does not expect HTML', async() => {
|
||||
expect(await makeRequest(scope, '/baz', 'default', {
|
||||
headers: {
|
||||
'Accept': 'text/plain, text/css',
|
||||
},
|
||||
mode: 'navigate',
|
||||
})).toBeNull();
|
||||
expect(await navRequest('/baz', {headers: {}})).toBeNull();
|
||||
server.assertSawRequestFor('/baz');
|
||||
|
||||
expect(await navRequest('/qux', {headers: {'Accept': 'text/plain'}})).toBeNull();
|
||||
server.assertSawRequestFor('/qux');
|
||||
});
|
||||
|
||||
async_it('does not redirect to index on a request with an extension', async() => {
|
||||
expect(await navRequest('/baz.html')).toBeNull();
|
||||
server.assertSawRequestFor('/baz.html');
|
||||
|
||||
// Only considers the last path segment when checking for a file extension.
|
||||
expect(await navRequest('/baz.html/qux')).toBe('this is foo');
|
||||
server.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
async_it('does not redirect to index if the URL contains `__`', async() => {
|
||||
expect(await navRequest('/baz/x__x')).toBeNull();
|
||||
server.assertSawRequestFor('/baz/x__x');
|
||||
|
||||
expect(await navRequest('/baz/x__x/qux')).toBeNull();
|
||||
server.assertSawRequestFor('/baz/x__x/qux');
|
||||
});
|
||||
|
||||
describe('(with custom `navigationUrls`)', () => {
|
||||
async_beforeEach(async() => {
|
||||
scope.updateServerState(serverUpdate);
|
||||
await driver.checkForUpdate();
|
||||
serverUpdate.clearRequests();
|
||||
});
|
||||
|
||||
async_it('redirects to index on a request that matches any positive pattern', async() => {
|
||||
expect(await navRequest('/foo/file0')).toBeNull();
|
||||
serverUpdate.assertSawRequestFor('/foo/file0');
|
||||
|
||||
expect(await navRequest('/foo/file1')).toBe('this is foo v2');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
|
||||
expect(await navRequest('/bar/file2')).toBe('this is foo v2');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
async_it(
|
||||
'does not redirect to index on a request that matches any negative pattern',
|
||||
async() => {
|
||||
expect(await navRequest('/ignored/file1')).toBe('this is not handled by the SW');
|
||||
serverUpdate.assertSawRequestFor('/ignored/file1');
|
||||
|
||||
expect(await navRequest('/ignored/dir/file2'))
|
||||
.toBe('this is not handled by the SW either');
|
||||
serverUpdate.assertSawRequestFor('/ignored/dir/file2');
|
||||
|
||||
expect(await navRequest('/ignored/directory/file2')).toBe('this is foo v2');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
});
|
||||
|
||||
async_it('strips URL query before checking `navigationUrls`', async() => {
|
||||
expect(await navRequest('/foo/file1?query=/a/b')).toBe('this is foo v2');
|
||||
serverUpdate.assertNoOtherRequests();
|
||||
|
||||
expect(await navRequest('/ignored/file1?query=/a/b'))
|
||||
.toBe('this is not handled by the SW');
|
||||
serverUpdate.assertSawRequestFor('/ignored/file1');
|
||||
|
||||
expect(await navRequest('/ignored/dir/file2?query=/a/b'))
|
||||
.toBe('this is not handled by the SW either');
|
||||
serverUpdate.assertSawRequestFor('/ignored/dir/file2');
|
||||
});
|
||||
|
||||
async_it('strips registration scope before checking `navigationUrls`', async() => {
|
||||
expect(await navRequest('http://localhost/ignored/file1'))
|
||||
.toBe('this is not handled by the SW');
|
||||
serverUpdate.assertSawRequestFor('/ignored/file1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -196,7 +196,7 @@ export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
|
||||
patterns: [],
|
||||
},
|
||||
],
|
||||
hashTable,
|
||||
navigationUrls: [], hashTable,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -287,7 +287,8 @@ export class ConfigBuilder {
|
||||
const hashTable = {};
|
||||
return {
|
||||
configVersion: 1,
|
||||
index: '/index.html', assetGroups, hashTable,
|
||||
index: '/index.html', assetGroups,
|
||||
navigationUrls: [], hashTable,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user