sw-testing-helpers
Version:
A set of helper files used to test Propel and sw-toolbox.
315 lines (292 loc) • 10.2 kB
JavaScript
/**
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/* eslint-env browser */
/**
* <p>SWUtils can be used in a <strong>webpage only</strong>.</p>
*
* <p>This is helpful for managing service worker registrations and
* cache creation / clean up between unit tests.</p>
*
* @example
* <script src="/node_modules/sw-testing-helper/browser/sw-utils.js"></script>
*
* <script>
* console.log(window.goog.swUtils);
* </script>
*/
class SWUtils {
/**
* SWUtils constructor should never be called directly.
*/
constructor() {
// The test counter ensures a unique scope between each test.
// testTime is used to ensure a unique scope between runs of
// the test suite - useful if manual testing parts of the
// suite in different tabs at the same time.
this._testCounter = 0;
this._testTime = new Date().getTime();
}
/**
* Helper method to determine when a specific state is achieved within
* a service worker (i.e. it becomes installed or activated).
*
* @private
*
* @param {ServiceWorkerRegistration} registration registration to watch
* for state changes
* @param {String} desiredState Name of the desired state to wait for
* @return {Promise} Resolves when the desired state is reached
*/
_onStateChangePromise(registration, desiredState) {
return new Promise((resolve, reject) => {
if (registration.installing === null) {
throw new Error('Service worker is not installing. Did you call ' +
'cleanState() to unregister this service?');
}
let serviceWorker = registration.installing;
// We unregister all service workers after each test - this should
// always trigger an install state change
let stateChangeListener = function(evt) {
if (evt.target.state === desiredState) {
serviceWorker.removeEventListener('statechange', stateChangeListener);
resolve();
return;
}
if (evt.target.state === 'redundant') {
serviceWorker.removeEventListener('statechange', stateChangeListener);
// Must call reject rather than throw error here due to this
// being inside the scope of the callback function stateChangeListener
reject(new Error('Installing servier worker became redundant'));
return;
}
};
serviceWorker.addEventListener('statechange', stateChangeListener);
});
}
/**
* <p>When a service worker is installed / activated using SWUtils,
* it'll be registered with a unqiue scope and an iframe will be
* created matching that scope (allowing it to be controlled by that
* service worker only).</p>
*
* <p>This method will get you the current iframe (if in the middle of a test)
* or create a new iframe.</p>
*
* @return {Promise.<HTMLElement>} Resolves to the current iframe being
* used for tests.
*/
getIframe() {
return new Promise(resolve => {
const existingIframe = document.querySelector('.js-test-iframe');
if (existingIframe) {
return resolve(existingIframe);
}
// This will be used as a unique service worker scope
this._testCounter++;
const newIframe = document.createElement('iframe');
newIframe.classList.add('js-test-iframe');
newIframe.addEventListener('load', () => {
resolve(newIframe);
});
newIframe.src = `/test/iframe/${this._testTime}${this._testCounter}`;
document.body.appendChild(newIframe);
});
}
/**
* Loop through all registrations for the current origin and unregister them.
*
* @return {Promise} Resolves once all promises are unregistered
*/
unregisterAllRegistrations() {
return navigator.serviceWorker.getRegistrations()
.then(registrations => {
return Promise.all(registrations.map(registration => {
return registration.unregister();
}));
});
}
/**
* Loop over all caches for the current origin and delete them.
*
* @return {Promise} Resolves once all caches are deleted
*/
clearAllCaches() {
return window.caches.keys()
.then(cacheNames => {
return Promise.all(cacheNames.map(cacheName => {
return window.caches.delete(cacheName);
}));
});
}
/**
* <p>Register a service worker to a unique scope and
* create an iframe it can control, wait until the service worker's
* install step has completed.</p>
*
* <p>Useful for testing events that occur in the install event (i.e.
* pre-caching of assets).</p>
*
* @param {String} swUrl The url to a service worker file to register
* @return {Promise.<HTMLElement>} Resolves once the service worker is
* installed and returns the iframe it controls.
*/
installSW(swUrl) {
return new Promise((resolve, reject) => {
let iframe;
this.getIframe()
.then(newIframe => {
let options = null;
if (newIframe) {
iframe = newIframe;
options = {scope: newIframe.contentWindow.location.pathname};
}
return navigator.serviceWorker.register(swUrl, options);
})
.then(registration => {
return this._onStateChangePromise(registration, 'installed');
})
.then(() => resolve(iframe))
.catch(err => reject(err));
});
}
/**
* <p>Register a service worker with a unique scope and
* create an iframe that can be controlled by that service worker, then
* wait until the service worker's activate step has completed.</p>
*
* <p>Useful for testing fetch events that can't occur until after the
* service worker has activated.</p>
*
* @param {String} swUrl The url to a service worker file to register
* @return {Promise.<HTMLElement>} Resolves once the service worker is
* activated and returns the iframe it controls.
*/
activateSW(swUrl) {
return new Promise((resolve, reject) => {
let iframe;
this.getIframe()
.then(newIframe => {
let options = null;
if (newIframe) {
options = {scope: newIframe.contentWindow.location.pathname};
iframe = newIframe;
}
return navigator.serviceWorker.register(swUrl, options);
})
.then(registration => {
return this._onStateChangePromise(registration, 'activated');
})
.then(() => resolve(iframe))
.catch(err => reject(err));
});
}
/**
* <p>Register a service worker with a unique scope and
* create an iframe that can be controlled by that service worker, then
* wait until the service worker takes control.</p>
*
* <p>Useful for testing behavior that assumes the page is already controlled
* by a service worker.</p>
*
* @param {String} swUrl The url to a service worker file to register
* @return {Promise.<HTMLElement>} Resolves with the iframe once the service
* worker has taken control.
*/
controlledBySW(swUrl) {
return this.activateSW(swUrl)
.then(iframe => {
return new Promise(resolve => {
let iframeSW = iframe.contentWindow.navigator.serviceWorker;
if (iframeSW.controller) {
resolve(iframe);
} else {
iframeSW.addEventListener('controllerchange',
() => resolve(iframe),
{once: true});
}
});
});
}
/**
* <p>Helper method that checks a cache with a specific name exists before
* retrieving all the cached responses inside of it.</p>
*
* <p>This is limited to text responses at the moment.</p>
*
* <p><strong>NOTE: </strong>This will reject if the cache doesn't exist.</p>
*
* @param {String} cacheName The name of the cache to get the contents from.
* @return {Promise.<Object>} Resolves to an object where the keys
* are URLs for the cache responses and the value is the text from the
* response. The promise rejects if the cache doesn't exist.
*/
getAllCachedAssets(cacheName) {
let cache = null;
return window.caches.has(cacheName)
.then(hasCache => {
if (!hasCache) {
throw new Error('Cache doesn\'t exist.');
}
return window.caches.open(cacheName);
})
.then(openedCache => {
cache = openedCache;
return cache.keys();
})
.then(cachedRequests => {
return Promise.all(cachedRequests.map(cachedRequest => {
return cache.match(cachedRequest)
.then(response => {
return {
request: cachedRequest,
response: response
};
});
}));
})
.then(cacheRequestResponsePairs => {
// This method extracts the response streams and pairs
// them with a url.
const output = {};
cacheRequestResponsePairs.forEach(cacheRequestResponsePair => {
output[cacheRequestResponsePair.request.url] =
cacheRequestResponsePair.response;
});
return output;
});
}
/**
* Helper to unregister all service workers and clear all caches.
*
* @return {Promise} Resolves once service workers are unregistered and caches
* are deleted.
*/
cleanState() {
return Promise.all([
this.unregisterAllRegistrations(),
this.clearAllCaches()
])
.then(() => {
const iframeList = document.querySelectorAll('.js-test-iframe');
for (let i = 0; i < iframeList.length; i++) {
iframeList[i].parentElement.removeChild(iframeList[i]);
}
});
}
}
window.goog = window.goog || {};
window.goog.swUtils = window.goog.swUtils || new SWUtils();