Files

1343 lines
43 KiB
JavaScript

/*
Copyright 2020 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {Deferred} from 'workbox-core/_private/Deferred.mjs';
import {logger} from 'workbox-core/_private/logger.mjs';
import {registerQuotaErrorCallback} from 'workbox-core/registerQuotaErrorCallback.mjs';
import {Strategy} from 'workbox-strategies/Strategy.mjs';
import {StrategyHandler} from 'workbox-strategies/StrategyHandler.mjs';
import {timeout} from 'workbox-core/_private/timeout.mjs';
import {
spyOnEvent,
eventDoneWaiting,
} from '../../../infra/testing/helpers/extendable-event-utils.mjs';
class TestStrategy extends Strategy {
_handle() {
return new Response('Test response');
}
}
const createStrategyHandler = (options) => {
const request = new Request('/handler-request');
const event = new FetchEvent('fetch', {request});
const url = new URL(request.url);
const params = {a: 1, b: 2};
spyOnEvent(event);
// First arg should be a `Strategy` instance, but the options just get
// copied to the instance anyway, so an object will suffice.
// return new StrategyHandler(options, {request, event});
return new StrategyHandler(new TestStrategy(options), {
request,
event,
url,
params,
});
};
describe(`StrategyHandler`, function () {
const sandbox = sinon.createSandbox();
beforeEach(async function () {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
sandbox.restore();
});
afterEach(async function () {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
sandbox.restore();
});
describe('constructor()', function () {
it(`should throw when called without an 'event' parameter in dev`, async function () {
if (process.env.NODE_ENV === 'production') {
return this.skip();
}
await expectError(
() => new StrategyHandler(new TestStrategy(), {}),
'incorrect-class',
(error) => {
expect(error.details)
.to.have.property('moduleName')
.that.equals('workbox-strategies');
expect(error.details)
.to.have.property('className')
.that.equals('StrategyHandler');
expect(error.details)
.to.have.property('funcName')
.that.equals('constructor');
expect(error.details)
.to.have.property('paramName')
.that.equals('options.event');
},
);
});
it('creates an object with the correct public properties', function () {
const handler = createStrategyHandler();
expect(handler.request).to.be.instanceOf(Request);
expect(handler.event).to.be.instanceOf(ExtendableEvent);
expect(handler.url).to.be.instanceOf(URL);
expect(handler.params).to.be.instanceOf(Object);
});
it('passes a deferred to event waitUntil (if passed an event)', function () {
const handler = createStrategyHandler();
expect(handler.event.waitUntil.callCount).to.equal(1);
expect(handler.event.waitUntil.firstCall.args[0]).to.be.instanceOf(
Promise,
);
});
});
describe('waitUntil()', function () {
it('adds promises to an internal queue', function () {
const handler = createStrategyHandler();
handler.waitUntil(Promise.resolve());
expect(handler._extendLifetimePromises).to.have.lengthOf(1);
handler.waitUntil(Promise.resolve());
handler.waitUntil(Promise.resolve());
expect(handler._extendLifetimePromises).to.have.lengthOf(3);
});
it('returns the passed promise', function () {
const handler = createStrategyHandler();
const promise = Promise.resolve();
expect(handler.waitUntil(promise)).to.equal(promise);
});
});
describe('doneWaiting()', function () {
it('returns a promise the resolves once all waitUntil promises have settled', async function () {
const handler = createStrategyHandler();
const spy = sandbox.spy();
const startTime = performance.now();
handler.waitUntil(timeout(200).then(spy));
handler.waitUntil(timeout(50).then(spy));
handler.waitUntil(timeout(100).then(spy));
await handler.doneWaiting();
expect(spy.callCount).to.equal(3);
expect(performance.now() - startTime >= 200).to.be.true;
});
});
describe('destroy()', function () {
it('resolves any waitUntil promises immediately', function (done) {
const handler = createStrategyHandler();
const deferred = new Deferred();
handler.waitUntil(deferred.promise);
eventDoneWaiting(handler.event).then(() => {
deferred.resolve();
done();
});
// Even though the promises passed to `handler.waitUntil()` hasn't
// resolved, calling `destroy()` should ensure the handler's event can.
handler.destroy();
});
});
// These tests were copied from `test-fetchWrapper` to ensure we don't
// lose any of the existing behavior in the update.
describe(`fetch()`, function () {
it(`should work with request string`, async function () {
const stub = sandbox.stub(self, 'fetch').callsFake(() => new Response());
const handler = createStrategyHandler();
await handler.fetch('/test/string');
expect(stub.callCount).to.equal(1);
const fetchRequest = stub.args[0][0];
expect(fetchRequest.url).to.equal(`${location.origin}/test/string`);
});
it(`should work with Request instance`, async function () {
const stub = sandbox.stub(self, 'fetch').callsFake(() => new Response());
const handler = createStrategyHandler();
await handler.fetch(new Request('/test/response'));
expect(stub.callCount).to.equal(1);
const fetchRequest = stub.args[0][0];
expect(fetchRequest.url).to.equal(`${location.origin}/test/response`);
});
it(`should use fetchOptions from the strategy`, async function () {
const stub = sandbox.stub(self, 'fetch').callsFake(() => new Response());
const fetchOptions = {
method: 'POST',
headers: {
Custom: 'Header',
},
body: 'Example Body',
};
const handler = createStrategyHandler({fetchOptions});
await handler.fetch('/test/fetchOptions');
expect(stub.callCount).to.equal(1);
const fetchArgs = stub.args[0];
expect(fetchArgs[0].url).to.equal(`${location.origin}/test/fetchOptions`);
expect(fetchArgs[1]).to.deep.equal(fetchOptions);
});
it(`should ignore fetchOptions when request.mode === 'navigate'`, async function () {
// Firefox crashes when we change the request mode, so skipping this test.
if (navigator.userAgent.match(/firefox/i)) {
this.skip();
}
// See https://github.com/GoogleChrome/workbox/issues/1796
const fetchStub = sandbox.stub(self, 'fetch').resolves(new Response());
const fetchOptions = {
headers: {
'X-Test': 'Header',
},
};
const request = new Request('/test/navigateFetchOptions');
// You normally can't generate a navigation request programmatically,
// but we can fake it with `Object.defineProperty()` after creation.
Object.defineProperty(request, 'mode', {value: 'navigate'});
const handler = createStrategyHandler({fetchOptions});
await handler.fetch(request);
expect(fetchStub.calledOnce).to.be.true;
expect(fetchStub.firstCall.args[0]).to.be.instanceOf(Request);
expect(fetchStub.firstCall.args[0].url).to.eql(request.url);
expect(fetchStub.firstCall.args[0].mode).to.eql('navigate');
expect(fetchStub.firstCall.args[1]).not.to.exist;
});
it(`should call the requestWillFetch callback in all strategy plugins and use the returned request`, async function () {
const fetchStub = sandbox
.stub(self, 'fetch')
.callsFake(() => new Response());
const stub1 = sandbox
.stub()
.returns(new Request('/test/requestWillFetch/1'));
const stub2 = sandbox
.stub()
.returns(new Request('/test/requestWillFetch/2'));
const firstPlugin = {requestWillFetch: stub1};
const secondPlugin = {requestWillFetch: stub2};
const handler = createStrategyHandler({
plugins: [firstPlugin, secondPlugin],
});
await handler.fetch('/test/requestWillFetch/0');
expect(stub1.callCount).equal(1);
expect(stub1.args[0][0].request.url).to.equal(
`${location.origin}/test/requestWillFetch/0`,
);
expect(stub1.args[0][0].event).to.equal(handler.event);
expect(stub2.callCount).equal(1);
expect(stub2.args[0][0].request.url).to.equal(
`${location.origin}/test/requestWillFetch/1`,
);
expect(stub2.args[0][0].event).to.equal(handler.event);
expect(fetchStub.callCount).to.equal(1);
const fetchRequest = fetchStub.args[0][0];
expect(fetchRequest.url).to.equal(
`${location.origin}/test/requestWillFetch/2`,
);
});
it(`should throw a meaningful error on bad requestWillFetch plugin`, async function () {
const fetchStub = sandbox
.stub(self, 'fetch')
.callsFake(() => new Response());
const errorPlugin = {
requestWillFetch: (request) => {
throw new Error('Injected Error from Test.');
},
};
const errorPluginSpy = sandbox.spy(errorPlugin, 'requestWillFetch');
const handler = createStrategyHandler({
plugins: [errorPlugin],
});
await expectError(
() => {
return handler.fetch('/test/requestWillFetch/0');
},
'plugin-error-request-will-fetch',
(err) => {
expect(err.details.thrownErrorMessage).to.exist;
expect(err.details.thrownErrorMessage).to.equal(
'Injected Error from Test.',
);
},
);
expect(errorPluginSpy.callCount).equal(1);
expect(fetchStub.callCount).to.equal(0);
});
it(`should call fetchDidFail method in plugins`, async function () {
sandbox.stub(self, 'fetch').callsFake(() => {
return Promise.reject(new Error('Injected Error.'));
});
const secondPlugin = {
fetchDidFail: sandbox
.stub()
.callsFake(({originalRequest, request, event, error}) => {
expect(originalRequest.url).to.equal(
`${location.origin}/test/failingRequest/0`,
);
expect(request.url).to.equal(
`${location.origin}/test/failingRequest/1`,
);
expect(error.message).to.equal('Injected Error.');
}),
};
const firstPlugin = {
requestWillFetch: ({request}) => {
return new Request('/test/failingRequest/1');
},
fetchDidFail: sandbox
.stub()
.callsFake(({originalRequest, request, event, error}) => {
// This should be called first
expect(secondPlugin.fetchDidFail.callCount).to.equal(0);
expect(originalRequest.url).to.equal(
`${location.origin}/test/failingRequest/0`,
);
expect(request.url).to.equal(
`${location.origin}/test/failingRequest/1`,
);
expect(error.message).to.equal('Injected Error.');
}),
};
const handler = createStrategyHandler({
plugins: [
firstPlugin,
{
// It should be able to handle plugins without the required method.
},
secondPlugin,
],
});
try {
await handler.fetch('/test/failingRequest/0');
throw new Error('No error thrown when it was expected.');
} catch (err) {
expect(err.message).to.equal('Injected Error.');
}
expect(firstPlugin.fetchDidFail.callCount).equal(1);
expect(secondPlugin.fetchDidFail.callCount).equal(1);
expect(self.fetch.callCount).to.equal(1);
const fetchRequest = self.fetch.args[0][0];
expect(fetchRequest.url).to.equal(
`${location.origin}/test/failingRequest/1`,
);
});
it(`should call the fetchDidSucceed method in plugins`, async function () {
const originalRequest = new Request('/testing');
sandbox.stub(self, 'fetch').resolves(
new Response('', {
headers: {
'x-count': 1,
},
}),
);
const fetchDidSucceed = sandbox.stub().callsFake(({response}) => {
const count = Number(response.headers.get('x-count'));
return new Response('', {
headers: {
'x-count': count + 1,
},
});
});
const handler = createStrategyHandler({
plugins: [
// Two plugins, both with the same callback.
{fetchDidSucceed},
{fetchDidSucceed},
],
});
const finalResponse = await handler.fetch(originalRequest);
expect(fetchDidSucceed.callCount).to.eql(2);
for (const args of fetchDidSucceed.args) {
expect(args[0].request).to.be.instanceOf(Request);
expect(args[0].response).to.be.instanceOf(Response);
expect(args[0].event).to.be.instanceOf(FetchEvent);
}
const finalCount = finalResponse.headers.get('x-count');
expect(finalCount).to.equal('3');
});
});
// These tests were copied from `test-cacheWrapper` to ensure we don't
// lose any of the existing behavior in the update.
describe(`.cachePut()`, function () {
it(`should work with a request and response`, async function () {
const testCache = await caches.open('TEST-CACHE');
const cacheOpenStub = sandbox.stub(self.caches, 'open');
const cachePutStub = sandbox.stub(testCache, 'put');
cacheOpenStub.callsFake(async (cacheName) => {
return testCache;
});
const putRequest = new Request('/test/string');
const putResponse = new Response('Response for /test/string');
const handler = createStrategyHandler({cacheName: 'TODO-CHANGE-ME'});
await handler.cachePut(putRequest, putResponse);
expect(cacheOpenStub.callCount).to.equal(1);
const cacheName1 = cacheOpenStub.args[0][0];
expect(cacheName1).to.equal('TODO-CHANGE-ME');
expect(cachePutStub.callCount).to.equal(1);
const cacheRequest = cachePutStub.args[0][0];
const cacheResponse = cachePutStub.args[0][1];
expect(cacheRequest).to.equal(putRequest);
expect(cacheResponse).to.equal(putResponse);
});
// This covers opaque responses (0) and partial content responses (206).
for (const status of [0, 206]) {
it(`should not cache response.status of ${status} by default`, async function () {
const cacheName = 'test-cache';
const testCache = await caches.open(cacheName);
const cacheOpenStub = sandbox
.stub(self.caches, 'open')
.resolves(testCache);
const cachePutSpy = sandbox.spy(testCache, 'put');
const putRequest = new Request('/test/string');
const putResponse = new Response('');
// You normally can't generate a 0 response status programmatically,
// but we can fake it with `Object.defineProperty()` after creation.
Object.defineProperty(putResponse, 'status', {value: status});
const handler = createStrategyHandler({cacheName});
await handler.cachePut(putRequest, putResponse);
expect(cacheOpenStub.callCount).to.equal(0);
expect(cachePutSpy.callCount).to.equal(0);
});
}
it(`should throw when trying to cache POST requests in dev mode`, async function () {
if (process.env.NODE_ENV === 'production') this.skip();
const testCache = await caches.open('TEST-CACHE');
const cacheOpenStub = sandbox.stub(self.caches, 'open');
const cachePutStub = sandbox.stub(testCache, 'put');
cacheOpenStub.callsFake(async (cacheName) => {
return testCache;
});
const putRequest = new Request('/test/string', {
method: 'POST',
});
const putResponse = new Response('Response for /test/string');
const handler = createStrategyHandler({cacheName: 'CACHE NAME'});
await expectError(async () => {
await handler.cachePut(putRequest, putResponse);
}, 'attempt-to-cache-non-get-request');
expect(cacheOpenStub.callCount).to.equal(0);
expect(cachePutStub.callCount).to.equal(0);
});
it(`should log when caching a response with a Vary: header in dev mode`, async function () {
if (process.env.NODE_ENV === 'production') {
this.skip();
}
const loggerSpy = sandbox.spy(logger, 'debug');
const request = new Request('/test/vary');
const response = new Response('Vary response', {
headers: {Vary: 'user-agent'},
});
const handler = createStrategyHandler({cacheName: 'vary-test'});
await handler.cachePut(request, response);
// Just a basic test for the logged string.
expect(loggerSpy.firstCall.args[0]).to.include('ignoreVary');
});
it(`should call cacheDidUpdate`, async function () {
const firstPlugin = {
cacheDidUpdate: () => {},
};
const secondPlugin = {
cacheDidUpdate: () => {},
};
const spyOne = sandbox.spy(firstPlugin, 'cacheDidUpdate');
const spyTwo = sandbox.spy(secondPlugin, 'cacheDidUpdate');
const putRequest = new Request('/test/string');
const putResponse = new Response('Response for /test/string', {
headers: {'x-id': '1'},
});
const handler = createStrategyHandler({
cacheName: 'TODO-CHANGE-ME',
plugins: [
firstPlugin,
{
// Should work without require functions
},
secondPlugin,
],
});
await handler.cachePut(putRequest, putResponse);
[spyOne, spyTwo].forEach((pluginSpy) => {
expect(pluginSpy.callCount).to.equal(1);
expect(pluginSpy.args[0][0].cacheName).to.equal('TODO-CHANGE-ME');
expect(pluginSpy.args[0][0].request).to.equal(putRequest);
expect(pluginSpy.args[0][0].oldResponse).to.equal(undefined);
// `newResponse` is cloned, so don't compare for object equality.
expect(pluginSpy.args[0][0].newResponse.headers.get('x-id')).to.equal(
'1',
);
// Reset so the spies are clean for next step in the test.
pluginSpy.resetHistory();
});
const putResponseUpdate = new Response(
'Response for /test/string number 2',
{
headers: {'x-id': '2'},
},
);
await handler.cachePut(putRequest, putResponseUpdate);
[spyOne, spyTwo].forEach((pluginSpy) => {
expect(pluginSpy.callCount).to.equal(1);
expect(pluginSpy.args[0][0].cacheName).to.equal('TODO-CHANGE-ME');
expect(pluginSpy.args[0][0].request).to.equal(putRequest);
// `oldResponse` and `newResponse` are cloned,
// so don't compare for object equality.
expect(pluginSpy.args[0][0].oldResponse.headers.get('x-id')).to.equal(
'1',
);
expect(pluginSpy.args[0][0].newResponse.headers.get('x-id')).to.equal(
'2',
);
});
});
it(`should call cacheWillUpdate`, async function () {
const firstPluginResponse = new Response('Response for /test/string/1');
const firstPlugin = {
cacheWillUpdate: () => {
return firstPluginResponse;
},
};
const secondPlugin = {
cacheWillUpdate: () => {
return new Response('Response for /test/string/2');
},
};
const spyOne = sandbox.spy(firstPlugin, 'cacheWillUpdate');
const spyTwo = sandbox.spy(secondPlugin, 'cacheWillUpdate');
const putRequest = new Request('/test/string');
const putResponse = new Response('Response for /test/string');
const handler = createStrategyHandler({
cacheName: 'TODO-CHANGE-ME',
plugins: [
firstPlugin,
{
// Should work without require functions
},
secondPlugin,
],
});
await handler.cachePut(putRequest, putResponse);
expect(spyOne.callCount).to.equal(1);
expect(
spyOne.calledWith(
sinon.match({
request: putRequest,
response: putResponse,
event: handler.event,
}),
),
).to.be.true;
expect(spyTwo.callCount).to.equal(1);
expect(
spyTwo.calledWith(
sinon.match({
request: putRequest,
response: firstPluginResponse,
event: handler.event,
}),
),
).to.be.true;
});
it(`should call cacheKeyWillBeUsed`, async function () {
const cacheName = 'cacheKeyWillBeUsed-test-cache';
const cache = await caches.open(cacheName);
sandbox.stub(caches, 'open').resolves(cache);
const cachePutStub = sandbox.stub(cache, 'put').resolves();
const firstPluginReturnValue = new Request('/firstPlugin');
const firstPlugin = {
cacheKeyWillBeUsed: () => firstPluginReturnValue,
};
const secondPlugin = {
// This string will be converted to a Request.
cacheKeyWillBeUsed: () => '/secondPlugin',
};
const spyOne = sandbox.spy(firstPlugin, 'cacheKeyWillBeUsed');
const spyTwo = sandbox.spy(secondPlugin, 'cacheKeyWillBeUsed');
const initialRequest = new Request('/noPlugin');
const response = new Response('Test response.');
const handler = createStrategyHandler({
cacheName,
plugins: [
firstPlugin,
{}, // Intentionally empty to ensure it's filtered out.
secondPlugin,
],
});
await handler.cachePut(initialRequest, response);
expect(
spyOne.calledOnceWith(
sinon.match({
mode: 'write',
request: initialRequest,
}),
),
).to.be.true;
expect(spyOne.thisValues[0]).to.eql(firstPlugin);
expect(
spyTwo.calledOnceWith(
sinon.match({
mode: 'write',
request: firstPluginReturnValue,
}),
),
).to.be.true;
expect(spyTwo.thisValues[0]).to.eql(secondPlugin);
expect(cachePutStub.calledOnce).to.be.true;
// Check the url of the Request passed to cache.put().
expect(cachePutStub.args[0][0].url).to.eql(
`${self.location.origin}/secondPlugin`,
);
expect(cachePutStub.args[0][1]).to.eql(response);
});
it(`should allow caching of posts if cacheKeyWillBeUsed returns a get request`, async function () {
const cacheName = 'cacheKeyWillBeUsed-test-cache';
const cache = await caches.open(cacheName);
sandbox.stub(caches, 'open').resolves(cache);
const cachePutStub = sandbox.stub(cache, 'put').resolves();
const firstPluginReturnValue = new Request('/firstPlugin', {
method: 'get',
});
const firstPlugin = {
cacheKeyWillBeUsed: () => firstPluginReturnValue,
};
const secondPlugin = {
// This string will be converted to a Request.
cacheKeyWillBeUsed: () => '/secondPlugin',
};
const spyOne = sandbox.spy(firstPlugin, 'cacheKeyWillBeUsed');
const spyTwo = sandbox.spy(secondPlugin, 'cacheKeyWillBeUsed');
const initialRequest = new Request('/noPlugin', {
method: 'post',
});
const response = new Response('Test response.');
const handler = createStrategyHandler({
cacheName,
plugins: [
firstPlugin,
{}, // Intentionally empty to ensure it's filtered out.
secondPlugin,
],
});
await handler.cachePut(initialRequest, response);
expect(
spyOne.calledOnceWith(
sinon.match({
mode: 'write',
request: initialRequest,
}),
),
).to.be.true;
expect(spyOne.thisValues[0]).to.eql(firstPlugin);
expect(
spyTwo.calledOnceWith(
sinon.match({
mode: 'write',
request: firstPluginReturnValue,
}),
),
).to.be.true;
expect(spyTwo.thisValues[0]).to.eql(secondPlugin);
expect(cachePutStub.calledOnce).to.be.true;
// Check the url of the Request passed to cache.put().
expect(cachePutStub.args[0][0].url).to.eql(
`${self.location.origin}/secondPlugin`,
);
expect(cachePutStub.args[0][1]).to.eql(response);
});
it(`should call the quota exceeded callbacks when there's a QuotaExceeded error`, async function () {
const callback1 = sandbox.stub();
registerQuotaErrorCallback(callback1);
const callback2 = sandbox.stub();
registerQuotaErrorCallback(callback2);
const cacheName = 'test-cache';
const testCache = await caches.open(cacheName);
sandbox.stub(self.caches, 'open').returns(Promise.resolve(testCache));
sandbox.stub(testCache, 'put').throws('QuotaExceededError');
const handler = createStrategyHandler({cacheName});
try {
await handler.cachePut('ignored', new Response());
throw new Error('Unexpected success.');
} catch (error) {
expect(error.name).to.eql('QuotaExceededError');
}
expect(callback1.calledOnce).to.be.true;
expect(callback2.calledOnce).to.be.true;
});
it(`should not call the quota exceeded callbacks when there's a non-QuotaExceeded error`, async function () {
const callback = sandbox.stub();
registerQuotaErrorCallback(callback);
const cacheName = 'test-cache';
const testCache = await caches.open(cacheName);
sandbox.stub(self.caches, 'open').returns(Promise.resolve(testCache));
sandbox.stub(testCache, 'put').throws('NetworkError');
const handler = createStrategyHandler({cacheName});
try {
await handler.cachePut('ignored', new Response());
throw new Error('Unexpected success.');
} catch (error) {
expect(error.name).to.eql('NetworkError');
}
expect(callback.called).to.be.false;
});
});
// These tests were copied from `test-cacheWrapper` to ensure we don't
// lose any of the existing behavior in the update.
describe(`.cacheMatch()`, function () {
it(`should use the matchOptions that were provided to put()`, async function () {
const matchOptions = {
ignoreSearch: true,
};
const cacheName = 'test-cache';
const matchSpy = sandbox.spy(self.caches, 'match');
const handler = createStrategyHandler({
cacheName,
matchOptions,
plugins: [
{
cacheDidUpdate: () => {},
},
],
});
await handler.cacheMatch(
new Request('/test/request'),
new Response('test'),
);
expect(matchSpy.calledOnce).to.be.true;
expect(matchSpy.args[0][1]).to.eql(
Object.assign({}, matchOptions, {
cacheName,
}),
);
});
it(`should call cachedResponseWillBeUsed`, async function () {
const options = {};
const matchCacheName = 'MATCH-CACHE-NAME';
const matchRequest = new Request('/test/string');
const matchResponse = new Response('Response for /test/string', {
headers: {'x-id': '1'},
});
const firstPluginResponse = new Response('Response for /test/string/1', {
headers: {'x-id': '2'},
});
const secondPluginResponse = new Response('Response for /test/string/2', {
headers: {'x-id': '3'},
});
const firstPlugin = {
cachedResponseWillBeUsed: ({
cacheName,
request,
matchOptions,
cachedResponse,
}) => {
expect(request).to.equal(matchRequest);
expect(cacheName).to.equal(matchCacheName);
expect(matchOptions).to.equal(options);
expect(cachedResponse.headers.get('x-id')).to.equal(
matchResponse.headers.get('x-id'),
);
return firstPluginResponse;
},
};
const secondPlugin = {
cachedResponseWillBeUsed: ({
cacheName,
request,
matchOptions,
cachedResponse,
}) => {
expect(request).to.equal(matchRequest);
expect(cacheName).to.equal(matchCacheName);
expect(matchOptions).to.equal(options);
expect(cachedResponse.headers.get('x-id')).to.equal(
firstPluginResponse.headers.get('x-id'),
);
return secondPluginResponse;
},
};
const spyOne = sandbox.spy(firstPlugin, 'cachedResponseWillBeUsed');
const spyTwo = sandbox.spy(secondPlugin, 'cachedResponseWillBeUsed');
const openCache = await caches.open(matchCacheName);
await openCache.put(matchRequest, matchResponse);
const handler = createStrategyHandler({
cacheName: matchCacheName,
matchOptions: options,
plugins: [
firstPlugin,
{
// Should work without require functions
},
secondPlugin,
],
});
const result = await handler.cacheMatch(matchRequest);
expect(result.headers.get('x-id')).to.equal(
secondPluginResponse.headers.get('x-id'),
);
expect(spyOne.callCount).to.equal(1);
expect(spyTwo.callCount).to.equal(1);
});
it(`should call cacheKeyWillBeUsed`, async function () {
const cacheName = 'cacheKeyWillBeUsed-test-cache';
const cacheMatchStub = sandbox
.stub(self.caches, 'match')
.resolves(new Response('Test response.'));
const firstPluginReturnValue = new Request('/firstPlugin');
const firstPlugin = {
cacheKeyWillBeUsed: () => firstPluginReturnValue,
};
const secondPlugin = {
// This string will be converted to a Request.
cacheKeyWillBeUsed: () => '/secondPlugin',
};
const spyOne = sandbox.spy(firstPlugin, 'cacheKeyWillBeUsed');
const spyTwo = sandbox.spy(secondPlugin, 'cacheKeyWillBeUsed');
const initialRequest = new Request('/noPlugin');
const handler = createStrategyHandler({
cacheName,
request: initialRequest,
plugins: [
firstPlugin,
{}, // Intentionally empty to ensure it's filtered out.
secondPlugin,
],
});
await handler.cacheMatch(initialRequest);
expect(
spyOne.calledOnceWith(
sinon.match({
mode: 'read',
request: initialRequest,
}),
),
).to.be.true;
expect(spyOne.thisValues[0]).to.eql(firstPlugin);
expect(
spyTwo.calledOnceWith(
sinon.match({
mode: 'read',
request: firstPluginReturnValue,
}),
),
).to.be.true;
expect(spyTwo.thisValues[0]).to.eql(secondPlugin);
expect(cacheMatchStub.calledOnce).to.be.true;
// Check the url of the Request passed to cache.put().
expect(cacheMatchStub.args[0][0].url).to.eql(
`${self.location.origin}/secondPlugin`,
);
});
});
describe('fetchAndCachePut()', function () {
it('calls fetch and then cachePut with the response', async function () {
const handler = createStrategyHandler();
sandbox.stub(self, 'fetch').resolves(new Response('fetch response'));
sandbox.stub(Cache.prototype, 'put');
sandbox.spy(handler, 'fetch');
sandbox.spy(handler, 'cachePut');
const response = await handler.fetchAndCachePut('/test');
expect(handler.fetch.callCount).to.equal(1);
expect(await response.text()).to.equal('fetch response');
expect(handler.cachePut.callCount).to.equal(1);
const spyCall = handler.cachePut.firstCall;
expect(spyCall.args[0]).to.equal('/test');
expect(await spyCall.args[1].text()).to.equal('fetch response');
});
it(`should work with request string`, async function () {
const handler = createStrategyHandler();
sandbox.stub(self, 'fetch').resolves(new Response('fetch response'));
const response = await handler.fetchAndCachePut('/url');
expect(await response.text()).to.equal('fetch response');
expect(self.fetch.args[0][0]).to.be.instanceOf(Request);
expect(self.fetch.args[0][0].url).to.equal(location.origin + '/url');
});
it(`should work with request object`, async function () {
const handler = createStrategyHandler();
sandbox.stub(self, 'fetch').resolves(new Response('fetch response'));
const response = await handler.fetchAndCachePut(new Request('/request'));
expect(await response.text()).to.equal('fetch response');
expect(self.fetch.args[0][0]).to.be.instanceOf(Request);
expect(self.fetch.args[0][0].url).to.equal(location.origin + '/request');
});
it(`should keep the handler alive until the cache put settles`, async function () {
const handler = createStrategyHandler();
sandbox.stub(self, 'fetch').resolves(new Response('fetch response'));
sandbox.spy(handler, 'cachePut');
sandbox.spy(handler, 'waitUntil');
await handler.fetchAndCachePut('/url');
await handler.doneWaiting();
expect(
handler.waitUntil.calledWith(handler.cachePut.firstCall.returnValue),
).to.be.true;
});
});
describe('hasCallback()', function () {
it('return true if the strategy contains at least one plugin with the given callback', function () {
const handler = createStrategyHandler({
plugins: [
{
handlerWillStart: sandbox.spy(),
},
{
handlerWillStart: sandbox.spy(),
handlerDidComplete: sandbox.spy(),
},
{
// Empty plugin.
},
{
requestWillFetch: sandbox.spy(),
},
],
});
expect(handler.hasCallback('handlerWillStart')).to.equal(true);
expect(handler.hasCallback('handlerDidComplete')).to.equal(true);
expect(handler.hasCallback('requestWillFetch')).to.equal(true);
expect(handler.hasCallback('cacheKeyWillBeUsed')).to.equal(false);
expect(handler.hasCallback('fetchDidFail')).to.equal(false);
});
});
describe('runCallbacks()', function () {
it('runs all matching callbacks with the correct arguments', async function () {
const spy = sandbox.spy();
const handler = createStrategyHandler({
plugins: [
{
handlerWillStart: spy,
},
{
// Empty plugin.
},
{
handlerWillStart: spy,
},
],
});
const request = new Request('test-request');
const event = new FetchEvent('fetch', {request});
await handler.runCallbacks('handlerWillStart', {request, event});
expect(spy.callCount).to.equal(2);
expect(spy.firstCall.args[0].request).to.equal(request);
expect(spy.firstCall.args[0].event).to.equal(event);
expect(spy.firstCall.args[0].state).to.be.instanceOf(Object);
expect(spy.secondCall.args[0].request).to.equal(request);
expect(spy.secondCall.args[0].event).to.equal(event);
expect(spy.secondCall.args[0].state).to.be.instanceOf(Object);
});
it('uses a unique state object per plugin', async function () {
const plugins = [
{
handlerWillStart: sandbox.spy(),
handlerDidComplete: sandbox.spy(),
},
{
handlerWillStart: sandbox.spy(),
},
{
// Empty plugin.
},
{
handlerWillStart: sandbox.spy(),
handlerDidComplete: sandbox.spy(),
},
];
const handler = createStrategyHandler({plugins});
const request = new Request('test-request');
const event = new FetchEvent('fetch', {request});
await handler.runCallbacks('handlerWillStart', {request, event});
// All of the `handlerWillStart` callbacks are from different plugins,
// so they should all have different state objects.
expect(plugins[0].handlerWillStart.firstCall.args[0].state).not.to.equal(
plugins[1].handlerWillStart.firstCall.args[0].state,
);
expect(plugins[1].handlerWillStart.firstCall.args[0].state).not.to.equal(
plugins[3].handlerWillStart.firstCall.args[0].state,
);
await handler.runCallbacks('handlerDidComplete', {request, event});
// All of the `handlerDidComplete` callbacks are from different plugins,
// so they should also all have different state objects.
expect(
plugins[0].handlerDidComplete.firstCall.args[0].state,
).not.to.equal(plugins[3].handlerDidComplete.firstCall.args[0].state);
// State objects from callbacks in the same plugin should be the same.
expect(plugins[0].handlerWillStart.firstCall.args[0].state).to.equal(
plugins[0].handlerDidComplete.firstCall.args[0].state,
);
expect(plugins[3].handlerWillStart.firstCall.args[0].state).to.equal(
plugins[3].handlerDidComplete.firstCall.args[0].state,
);
});
});
describe('iterateCallbacks()', function () {
it('loops through all matching callbacks', async function () {
const spy = sandbox.spy();
const handler = createStrategyHandler({
plugins: [
{
handlerWillStart: spy,
},
{
// Empty plugin.
},
{
handlerWillStart: spy,
},
],
});
const request = new Request('/test-request');
const event = new FetchEvent('fetch', {request});
for (const callback of handler.iterateCallbacks('handlerWillStart')) {
await callback({event, request});
expect(spy.callCount).to.equal(1);
expect(spy.firstCall.args[0].request).to.equal(request);
expect(spy.firstCall.args[0].event).to.equal(event);
expect(spy.firstCall.args[0].state).to.be.instanceOf(Object);
spy.resetHistory();
}
});
it('uses a unique state object per plugin', async function () {
const plugins = [
{
handlerWillStart: sandbox.spy(),
handlerDidComplete: sandbox.spy(),
},
{
handlerWillStart: sandbox.spy(),
},
{
// Empty plugin.
},
{
handlerWillStart: sandbox.spy(),
handlerDidComplete: sandbox.spy(),
},
];
const handler = createStrategyHandler({plugins});
const request = new Request('test-request');
const event = new FetchEvent('fetch', {request});
for (const callback of handler.iterateCallbacks('handlerWillStart')) {
await callback({event, request});
}
// All of the `handlerWillStart` callbacks are from different plugins,
// so they should all have different state objects.
expect(plugins[0].handlerWillStart.firstCall.args[0].state).not.to.equal(
plugins[1].handlerWillStart.firstCall.args[0].state,
);
expect(plugins[1].handlerWillStart.firstCall.args[0].state).not.to.equal(
plugins[3].handlerWillStart.firstCall.args[0].state,
);
for (const callback of handler.iterateCallbacks('handlerDidComplete')) {
await callback({event, request});
}
// All of the `handlerDidComplete` callbacks are from different plugins,
// so they should also all have different state objects.
expect(
plugins[0].handlerDidComplete.firstCall.args[0].state,
).not.to.equal(plugins[3].handlerDidComplete.firstCall.args[0].state);
// State objects from callbacks in the same plugin should be the same.
expect(plugins[0].handlerWillStart.firstCall.args[0].state).to.equal(
plugins[0].handlerDidComplete.firstCall.args[0].state,
);
expect(plugins[3].handlerWillStart.firstCall.args[0].state).to.equal(
plugins[3].handlerDidComplete.firstCall.args[0].state,
);
});
it('returns the value returned by the callback', async function () {
let request = new Request('/test-request');
const event = new FetchEvent('fetch', {request});
const handler = createStrategyHandler({
plugins: [
{
handlerWillStart({event, request}) {
return new Request(request.url + '+1');
},
},
{
// Empty plugin.
},
{
handlerWillStart({event, request}) {
return new Request(request.url + '+2');
},
},
],
});
for (const callback of handler.iterateCallbacks('handlerWillStart')) {
request = await callback({event, request});
}
expect(request.url).to.equal(location.origin + '/test-request+1+2');
});
});
describe(`getCacheKey`, function () {
it(`returns the cackeKey after applying plugins`, async function () {
const request = new Request('/test');
const handler = createStrategyHandler({
plugins: [
{
cacheKeyWillBeUsed({mode, request}) {
return new Request(request.url + '+1');
},
},
{
cacheKeyWillBeUsed({mode, request}) {
return mode === 'read'
? new Request(request.url + '+read')
: request;
},
},
{
cacheKeyWillBeUsed({mode, request}) {
return mode === 'write'
? new Request(request.url + '+write')
: request;
},
},
],
});
const readCacheKey = await handler.getCacheKey(request, 'read');
expect(readCacheKey.url).to.equal(location.origin + '/test+1+read');
const writeCacheKey = await handler.getCacheKey(request, 'write');
expect(writeCacheKey.url).to.equal(location.origin + '/test+1+write');
});
it(`caches the key using request + mode avoid repeat invocations`, async function () {
const request = new Request('/test');
const plugin = {
cacheKeyWillBeUsed({mode, request}) {
return new Request(request.url + '+' + mode);
},
};
sandbox.spy(plugin, 'cacheKeyWillBeUsed');
const handler = createStrategyHandler({
plugins: [plugin],
});
const readCacheKey = await handler.getCacheKey(request, 'read');
expect(readCacheKey.url).to.equal(location.origin + '/test+read');
expect(plugin.cacheKeyWillBeUsed.callCount).to.equal(1);
await handler.getCacheKey(request, 'read');
await handler.getCacheKey(request, 'read');
expect(plugin.cacheKeyWillBeUsed.callCount).to.equal(1);
const writeCacheKey = await handler.getCacheKey(request, 'write');
expect(writeCacheKey.url).to.equal(location.origin + '/test+write');
expect(plugin.cacheKeyWillBeUsed.callCount).to.equal(2);
await handler.getCacheKey(request, 'write');
await handler.getCacheKey(request, 'write');
expect(plugin.cacheKeyWillBeUsed.callCount).to.equal(2);
// See https://github.com/GoogleChrome/workbox/issues/2972
const secondRequest = new Request('/test2');
await handler.getCacheKey(secondRequest, 'read');
expect(plugin.cacheKeyWillBeUsed.callCount).to.equal(3);
await handler.getCacheKey(secondRequest, 'write');
await handler.getCacheKey(secondRequest, 'write');
expect(plugin.cacheKeyWillBeUsed.callCount).to.equal(4);
});
});
});