From 8d21f5cf035812072ca64f7f7cd70dfd718b58ea Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Wed, 23 Nov 2016 14:20:30 -0800 Subject: [PATCH 01/19] Adding sw-precaching --- .../test/browser-unit/library-namespace.js | 4 +- packages/sw-precaching/src/index.js | 13 ++- .../sw-precaching/src/lib/error-factory.js | 22 +++++ .../src/lib/revisioned-cache-manager.js | 44 +++++++++ packages/sw-precaching/test/.eslintrc | 10 ++ .../test/automated-test-suite.js | 97 +++++++++++++++++++ .../test/browser-unit/index.html | 76 +++++++++++++++ .../test/browser-unit/library-namespace.js | 59 +++++++++++ .../test/browser-unit/sw-unit/basic.js | 25 +++++ .../sw-unit/revisioned-cache-manager.js | 66 +++++++++++++ 10 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 packages/sw-precaching/src/lib/error-factory.js create mode 100644 packages/sw-precaching/src/lib/revisioned-cache-manager.js create mode 100644 packages/sw-precaching/test/.eslintrc create mode 100644 packages/sw-precaching/test/automated-test-suite.js create mode 100644 packages/sw-precaching/test/browser-unit/index.html create mode 100644 packages/sw-precaching/test/browser-unit/library-namespace.js create mode 100644 packages/sw-precaching/test/browser-unit/sw-unit/basic.js create mode 100644 packages/sw-precaching/test/browser-unit/sw-unit/revisioned-cache-manager.js diff --git a/packages/sw-lib/test/browser-unit/library-namespace.js b/packages/sw-lib/test/browser-unit/library-namespace.js index 975026a2b..a4a98b2b9 100644 --- a/packages/sw-lib/test/browser-unit/library-namespace.js +++ b/packages/sw-lib/test/browser-unit/library-namespace.js @@ -17,6 +17,8 @@ describe('Test Behaviors of Loading the Script', function() { this.timeout(5 * 60 * 1000); it('should print an error when added to the window.', function() { + this.timeout(2000); + return new Promise((resolve, reject) => { window.onerror = (msg, url, lineNo, columnNo, error) => { window.onerror = null; @@ -31,7 +33,7 @@ describe('Test Behaviors of Loading the Script', function() { }; const scriptElement = document.createElement('script'); - scriptElement.src = '/packages/sw-lib/build/sw-lib.min.js'; + scriptElement.src = '/packages/sw-precaching/build/sw-lib.min.js'; scriptElement.addEventListener('error', (event) => { reject(); }); diff --git a/packages/sw-precaching/src/index.js b/packages/sw-precaching/src/index.js index cc74c75f1..cdf9d8b6c 100644 --- a/packages/sw-precaching/src/index.js +++ b/packages/sw-precaching/src/index.js @@ -18,6 +18,17 @@ * * @module sw-precaching */ -export default function DO_NOT_USE() { +import ErrorFactory from './lib/error-factory'; +import RevisionedCacheManager from './lib/revisioned-cache-manager'; + +import assert from '../../../lib/assert.js'; + +if (!assert.isSWEnv()) { + // We are not running in a service worker, print error message + throw ErrorFactory.createError('not-in-sw'); } + +export { + RevisionedCacheManager +}; diff --git a/packages/sw-precaching/src/lib/error-factory.js b/packages/sw-precaching/src/lib/error-factory.js new file mode 100644 index 000000000..f71ea0be7 --- /dev/null +++ b/packages/sw-precaching/src/lib/error-factory.js @@ -0,0 +1,22 @@ +/* + 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. +*/ + +import ErrorFactory from '../../../../lib/error-factory'; + +const errors = { + 'not-in-sw': 'sw-precaching must be loaded in your service worker file.', +}; + +export default new ErrorFactory(errors); diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js new file mode 100644 index 000000000..66619764f --- /dev/null +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -0,0 +1,44 @@ +/* + 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. +*/ + +import assert from '../../../../lib/assert'; + +class RevisionedCacheManager { + constructor() { + this._eventsRegistered = false; + } + + cache({revisionedFiles} = {}) { + assert.isInstance({revisionedFiles: revisionedFiles}, Array); + // this._registerEvents(); + } + + _registerEvents() { + if (this._eventsRegistered) { + // Only need to register events once. + return; + } + + self.addEventListener('install', function() { + console.log('Install event fired'); + }); + + self.addEventListener('activate', function() { + console.log('activate event fired'); + }); + } +} + +export default RevisionedCacheManager; diff --git a/packages/sw-precaching/test/.eslintrc b/packages/sw-precaching/test/.eslintrc new file mode 100644 index 000000000..0c1ea1fec --- /dev/null +++ b/packages/sw-precaching/test/.eslintrc @@ -0,0 +1,10 @@ +{ + "rules": { + "no-console": 0, + "max-len": 0, + "no-invalid-this": 0, + }, + "env": { + "mocha": true + } +} diff --git a/packages/sw-precaching/test/automated-test-suite.js b/packages/sw-precaching/test/automated-test-suite.js new file mode 100644 index 000000000..4b85246e4 --- /dev/null +++ b/packages/sw-precaching/test/automated-test-suite.js @@ -0,0 +1,97 @@ +/* + 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. +*/ + +const seleniumAssistant = require('selenium-assistant'); +const swTestingHelpers = require('sw-testing-helpers'); +const testServer = new swTestingHelpers.TestServer(); + +require('chromedriver'); +require('operadriver'); +require('geckodriver'); + +const RETRIES = 4; +const TIMEOUT = 10 * 1000; + +const setupTestSuite = (assistantDriver) => { + describe(`sw-praching Tests in ${assistantDriver.getPrettyName()}`, function() { + this.retries(RETRIES); + this.timeout(TIMEOUT); + + let globalDriverBrowser; + let baseTestUrl; + + // Set up the web server before running any tests in this suite. + before(function() { + return testServer.startServer('.').then((portNumber) => { + baseTestUrl = `http://localhost:${portNumber}/packages/sw-precaching`; + }); + }); + + // Kill the web server once all tests are complete. + after(function() { + return testServer.killServer(); + }); + + afterEach(function() { + return seleniumAssistant.killWebDriver(globalDriverBrowser) + .then(() => { + globalDriverBrowser = null; + }); + }); + + it('should pass all browser based unit tests', function() { + return assistantDriver.getSeleniumDriver() + .then((driver) => { + globalDriverBrowser = driver; + }) + .then(() => { + return swTestingHelpers.mochaUtils.startWebDriverMochaTests( + assistantDriver.getPrettyName(), + globalDriverBrowser, + `${baseTestUrl}/test/browser-unit/` + ); + }) + .then((testResults) => { + if (testResults.failed.length > 0) { + const errorMessage = swTestingHelpers.mochaUtils.prettyPrintErrors( + assistantDriver.getPrettyName(), + testResults + ); + + throw new Error(errorMessage); + } + }); + }); + }); +}; + +const availableBrowsers = seleniumAssistant.getAvailableBrowsers(); +availableBrowsers.forEach((browser) => { + switch(browser.getSeleniumBrowserId()) { + case 'chrome': + case 'firefox': + case 'opera': + if (browser.getSeleniumBrowserId() === 'opera' && + browser.getVersionNumber() <= 43) { + console.log(`Skipping Opera <= 43 due to driver issues.`); + return; + } + setupTestSuite(browser); + break; + default: + console.log(`Skipping tests for ${browser.getSeleniumBrowserId()}`); + break; + } +}); diff --git a/packages/sw-precaching/test/browser-unit/index.html b/packages/sw-precaching/test/browser-unit/index.html new file mode 100644 index 000000000..ac5424112 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/index.html @@ -0,0 +1,76 @@ + + + + + SW Helpers Tests + + + + + + + +
+ + + + + + + + + + + + + + + + + + diff --git a/packages/sw-precaching/test/browser-unit/library-namespace.js b/packages/sw-precaching/test/browser-unit/library-namespace.js new file mode 100644 index 000000000..793c981aa --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/library-namespace.js @@ -0,0 +1,59 @@ +/* + 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. +*/ +describe('Test Behaviors of Loading the Script', function() { + this.timeout(5 * 60 * 1000); + + it('should print an error when added to the window.', function() { + this.timeout(2000); + + return new Promise((resolve, reject) => { + window.onerror = (msg, url, lineNo, columnNo, error) => { + window.onerror = null; + + if (error.name === 'not-in-sw') { + resolve(); + return true; + } else { + reject('Unexpected error received.'); + return false; + } + }; + + const scriptElement = document.createElement('script'); + scriptElement.src = '/packages/sw-precaching/build/sw-precaching.min.js'; + scriptElement.addEventListener('error', (event) => { + console.log(event); + reject(); + }); + document.head.appendChild(scriptElement); + }); + }); + + const swUnitTests = [ + 'sw-unit/basic.js', + 'sw-unit/revisioned-cache-manager.js', + ]; + swUnitTests.forEach((swUnitTestPath) => { + it(`should perform ${swUnitTestPath} sw tests`, function() { + return window.goog.mochaUtils.startServiceWorkerMochaTests(swUnitTestPath) + .then((testResults) => { + if (testResults.failed.length > 0) { + const errorMessage = window.goog.mochaUtils.prettyPrintErrors(swUnitTestPath, testResults); + throw new Error(errorMessage); + } + }); + }); + }); +}); diff --git a/packages/sw-precaching/test/browser-unit/sw-unit/basic.js b/packages/sw-precaching/test/browser-unit/sw-unit/basic.js new file mode 100644 index 000000000..5ccc3e128 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/sw-unit/basic.js @@ -0,0 +1,25 @@ +importScripts('/node_modules/mocha/mocha.js'); +importScripts('/node_modules/chai/chai.js'); +importScripts('/node_modules/sw-testing-helpers/browser/mocha-utils.js'); + +importScripts('/packages/sw-precaching/build/sw-precaching.min.js'); + +/* global goog */ + +const expect = self.chai.expect; +self.chai.should(); +mocha.setup({ + ui: 'bdd', + reporter: null, +}); + +describe('Test Library Surface', function() { + it('should be accessible via goog.precaching', function() { + expect(goog.precaching).to.exist; + }); + + // TODO Test options + it('should have RevisionedCacheManager via goog.precaching', function() { + expect(goog.precaching.RevisionedCacheManager).to.exist; + }); +}); diff --git a/packages/sw-precaching/test/browser-unit/sw-unit/revisioned-cache-manager.js b/packages/sw-precaching/test/browser-unit/sw-unit/revisioned-cache-manager.js new file mode 100644 index 000000000..4ddcbd438 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/sw-unit/revisioned-cache-manager.js @@ -0,0 +1,66 @@ +importScripts('/node_modules/mocha/mocha.js'); +importScripts('/node_modules/chai/chai.js'); +importScripts('/node_modules/sw-testing-helpers/browser/mocha-utils.js'); + +importScripts('/packages/sw-precaching/build/sw-precaching.min.js'); + +/* global goog */ + +const expect = self.chai.expect; +self.chai.should(); + +mocha.setup({ + ui: 'bdd', + reporter: null, +}); + +describe('Test RevisionedCacheManager', function() { + const badRevisionFileInputs = [ + undefined, + '', + '/example.js', + {}, + {path: 'hello'}, + {revision: '0987'}, + true, + false, + 123, + ]; + badRevisionFileInputs.forEach((badInput) => { + it(`should handle bad cache({revisionedFiles='${badInput}'}) input`, function() { + expect(() => { + const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); + revisionedCacheManager.cache({ + revisionedFiles: badInput, + }); + }).to.throw('instance of \'Array\''); + }); + + it(`should handle bad cache('${badInput}') input`, function() { + expect(() => { + const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); + revisionedCacheManager.cache(badInput); + }).to.throw('instance of \'Array\''); + }); + }); + + it(`should handle bad cache('[]') input`, function() { + expect(() => { + const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); + revisionedCacheManager.cache([]); + }).to.throw('instance of \'Array\''); + }); + + const nullUndefinedError = 'Cannot match against \'undefined\' or \'null\''; + it(`should handle null / undefined inputs`, function() { + expect(() => { + const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); + revisionedCacheManager.cache({revisionedFiles: null}); + }).to.throw('instance of \'Array\''); + + expect(() => { + const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); + revisionedCacheManager.cache(null); + }).to.throw(nullUndefinedError); + }); +}); From 770f3c03847906fdeff7518f72d1eaa57ba1542f Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Wed, 23 Nov 2016 16:47:41 -0800 Subject: [PATCH 02/19] Started to add some caching logic. Needs errors cases covered and a test to confirm existing cached assets aren't re-cached --- .../sw-precaching/src/lib/error-factory.js | 2 + .../sw-precaching/src/lib/install-handler.js | 94 +++++++++++-------- .../src/lib/revisioned-cache-manager.js | 17 ++-- .../test/browser-unit/cache-testing.js | 45 +++++++++ .../test/browser-unit/data/basic-cache/sw.js | 6 ++ .../data/files/file-set-1/1.1234.txt | 1 + .../data/files/file-set-1/2.1234.txt | 1 + .../browser-unit/data/files/file-set-1/3.txt | 1 + .../browser-unit/data/files/file-set-1/4.txt | 1 + .../test/browser-unit/data/test-data.js | 12 +++ .../test/browser-unit/index.html | 3 + .../sw-unit/revisioned-cache-manager.js | 3 +- 12 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 packages/sw-precaching/test/browser-unit/cache-testing.js create mode 100644 packages/sw-precaching/test/browser-unit/data/basic-cache/sw.js create mode 100644 packages/sw-precaching/test/browser-unit/data/files/file-set-1/1.1234.txt create mode 100644 packages/sw-precaching/test/browser-unit/data/files/file-set-1/2.1234.txt create mode 100644 packages/sw-precaching/test/browser-unit/data/files/file-set-1/3.txt create mode 100644 packages/sw-precaching/test/browser-unit/data/files/file-set-1/4.txt create mode 100644 packages/sw-precaching/test/browser-unit/data/test-data.js diff --git a/packages/sw-precaching/src/lib/error-factory.js b/packages/sw-precaching/src/lib/error-factory.js index f71ea0be7..6028e4c57 100644 --- a/packages/sw-precaching/src/lib/error-factory.js +++ b/packages/sw-precaching/src/lib/error-factory.js @@ -17,6 +17,8 @@ import ErrorFactory from '../../../../lib/error-factory'; const errors = { 'not-in-sw': 'sw-precaching must be loaded in your service worker file.', + 'assets-not-an-array': 'Assets passed to \'install-handler.js\' must be ' + + 'an array', }; export default new ErrorFactory(errors); diff --git a/packages/sw-precaching/src/lib/install-handler.js b/packages/sw-precaching/src/lib/install-handler.js index c95c2c19e..2477ffd75 100644 --- a/packages/sw-precaching/src/lib/install-handler.js +++ b/packages/sw-precaching/src/lib/install-handler.js @@ -13,49 +13,63 @@ limitations under the License. */ -import assert from '../../../../lib/assert'; -import {version, defaultCacheId} from './constants'; +import ErrorFactory from './error-factory'; +import {version, defaultCacheId, hashParamName} from './constants'; -const setOfCachedUrls = (cache) => { +const getCachedUrls = (cache) => { return cache.keys() - .then((requests) => requests.map((request) => request.url)) - .then((urls) => new Set(urls)); + .then((requests) => { + return requests.map((request) => request.url); + }); }; -const urlsToCacheKeys = (precacheConfig) => new Map( - /** urls.map(item => { - var relativeUrl = item[0]; - var hash = item[1]; - var absoluteUrl = new URL(relativeUrl, self.location); - var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, /a/); - return [absoluteUrl.toString(), cacheKey]; - })**/ -); - -export default ({assetsAndHahes, cacheId} = {}) => { - assert.isType(assetsAndHahes, 'array'); - - self.addEventListener('install', (event) => { - const cacheName = - `${cacheId || defaultCacheId}-${version}-${self.registration.scope}`; - - event.waitUntil( - caches.open(cacheName).then((cache) => { - return setOfCachedUrls(cache).then((cachedUrls) => { - return Promise.all( - Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { - // If we don't have a key matching url in the cache already, - // add it. - if (!cachedUrls.has(cacheKey)) { - return cache.add( - new Request(cacheKey, {credentials: 'same-origin'})); - } - - return Promise.resolve(); - }) - ); - }); - }) - ); +const createRevisionedUrl = (assetAndHash) => { + const absoluteUrl = new URL(assetAndHash.path, self.location); + absoluteUrl.search += (absoluteUrl.search ? '&' : '') + + encodeURIComponent(hashParamName) + '=' + + encodeURIComponent(assetAndHash.revision); + return absoluteUrl.toString(); +}; + +const parseToRevisionedUrls = (assetsAndHahes) => { + return assetsAndHahes.map((assetAndHash) => { + let assetUrl = assetAndHash; + + if (typeof assetAndHash !== 'string') { + // Asset is an object with {path: '', revision: ''} + assetUrl = createRevisionedUrl(assetAndHash); + } + + return assetUrl; + }); +}; + +const installHandler = ({assetsAndHahes, cacheId} = {}) => { + if (!Array.isArray(assetsAndHahes)) { + throw ErrorFactory.createError('assets-not-an-array'); + } + + const cacheName = + `${cacheId || defaultCacheId}-${version}-${self.registration.scope}`; + const revisionedUrls = parseToRevisionedUrls(assetsAndHahes); + + return caches.open(cacheName) + .then((cache) => { + return getCachedUrls(cache) + .then((cachedUrls) => { + const cacheAddPromises = revisionedUrls.map((revisionedUrl) => { + if (cachedUrls.includes(revisionedUrl)) { + return Promise.resolve(); + } + + return cache.add(new Request(revisionedUrl, { + credentials: 'same-origin', + })); + }); + + return Promise.all(cacheAddPromises); + }); }); }; + +export default installHandler; diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js index 66619764f..9bfadd7ea 100644 --- a/packages/sw-precaching/src/lib/revisioned-cache-manager.js +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -14,15 +14,20 @@ */ import assert from '../../../../lib/assert'; +import installHandler from './install-handler'; class RevisionedCacheManager { constructor() { this._eventsRegistered = false; + this._cachedAssets = []; } cache({revisionedFiles} = {}) { assert.isInstance({revisionedFiles: revisionedFiles}, Array); - // this._registerEvents(); + + this._cachedAssets = this._cachedAssets.concat(revisionedFiles); + + this._registerEvents(); } _registerEvents() { @@ -31,12 +36,12 @@ class RevisionedCacheManager { return; } - self.addEventListener('install', function() { - console.log('Install event fired'); - }); + this._eventsRegistered = true; - self.addEventListener('activate', function() { - console.log('activate event fired'); + self.addEventListener('install', (event) => { + event.waitUntil( + installHandler({assetsAndHahes: this._cachedAssets}) + ); }); } } diff --git a/packages/sw-precaching/test/browser-unit/cache-testing.js b/packages/sw-precaching/test/browser-unit/cache-testing.js new file mode 100644 index 000000000..7e57eb6d6 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/cache-testing.js @@ -0,0 +1,45 @@ +/* global goog, expect */ + +describe('sw-precaching Test Revisioned Caching', function() { + beforeEach(function() { + return window.goog.swUtils.cleanState(); + }); + + after(function() { + // return window.goog.swUtils.cleanState(); + }); + + it('should cache files', function() { + return window.goog.swUtils.activateSW('data/basic-cache/sw.js') + .then(() => { + return window.caches.keys(); + }) + .then((cacheNames) => { + cacheNames.length.should.equal(1); + return window.caches.open(cacheNames[0]); + }) + .then((cache) => { + return cache.keys(); + }) + .then((cachedResponses) => { + cachedResponses.length.should.equal(goog.__TEST_DATA.EXAMPLE_REVISIONED_FILES.length); + + goog.__TEST_DATA.EXAMPLE_REVISIONED_FILES.forEach((assetAndHash) => { + let matchingResponse = null; + cachedResponses.forEach((cachedResponse) => { + let desiredPath = assetAndHash; + if (typeof assetAndHash !== 'string') { + desiredPath = assetAndHash.path; + } + + if (cachedResponse.url.indexOf(desiredPath) !== -1) { + matchingResponse = cachedResponse; + return; + } + }); + + expect(matchingResponse).to.exist; + }); + }); + }); +}); diff --git a/packages/sw-precaching/test/browser-unit/data/basic-cache/sw.js b/packages/sw-precaching/test/browser-unit/data/basic-cache/sw.js new file mode 100644 index 000000000..03feffa51 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/data/basic-cache/sw.js @@ -0,0 +1,6 @@ +/* global goog */ +importScripts('/packages/sw-precaching/test/browser-unit/data/test-data.js'); +importScripts('/packages/sw-precaching/build/sw-precaching.min.js'); + +const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); +revisionedCacheManager.cache({revisionedFiles: goog.__TEST_DATA.EXAMPLE_REVISIONED_FILES}); diff --git a/packages/sw-precaching/test/browser-unit/data/files/file-set-1/1.1234.txt b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/1.1234.txt new file mode 100644 index 000000000..622845909 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/1.1234.txt @@ -0,0 +1 @@ +1.1234.txt diff --git a/packages/sw-precaching/test/browser-unit/data/files/file-set-1/2.1234.txt b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/2.1234.txt new file mode 100644 index 000000000..34e886c9f --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/2.1234.txt @@ -0,0 +1 @@ +2.1234.txt diff --git a/packages/sw-precaching/test/browser-unit/data/files/file-set-1/3.txt b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/3.txt new file mode 100644 index 000000000..270b8ea76 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/3.txt @@ -0,0 +1 @@ +3.txt diff --git a/packages/sw-precaching/test/browser-unit/data/files/file-set-1/4.txt b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/4.txt new file mode 100644 index 000000000..d6c93164c --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/data/files/file-set-1/4.txt @@ -0,0 +1 @@ +4.txt diff --git a/packages/sw-precaching/test/browser-unit/data/test-data.js b/packages/sw-precaching/test/browser-unit/data/test-data.js new file mode 100644 index 000000000..da0685487 --- /dev/null +++ b/packages/sw-precaching/test/browser-unit/data/test-data.js @@ -0,0 +1,12 @@ +const pathPrefix = '/packages/sw-precaching/test/browser-unit/data/files'; +const EXAMPLE_REVISIONED_FILES = [ + pathPrefix + '/file-set-1/1.1234.txt', + pathPrefix + '/file-set-1/2.1234.txt', + {path: pathPrefix + '/file-set-1/3.txt', revision: '1234'}, + {path: pathPrefix + '/file-set-1/4.txt', revision: '1234'}, +]; + +self.goog = self.goog || {}; +self.goog.__TEST_DATA = { + EXAMPLE_REVISIONED_FILES, +}; diff --git a/packages/sw-precaching/test/browser-unit/index.html b/packages/sw-precaching/test/browser-unit/index.html index ac5424112..73c7aacf4 100644 --- a/packages/sw-precaching/test/browser-unit/index.html +++ b/packages/sw-precaching/test/browser-unit/index.html @@ -50,10 +50,13 @@ }); chai.should(); + const expect = chai.expect; + + - - diff --git a/packages/sw-precaching/demo/service-worker.js b/packages/sw-precaching/demo/service-worker.js deleted file mode 100755 index 8bbce2928..000000000 --- a/packages/sw-precaching/demo/service-worker.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-env worker, serviceworker */ -/* global goog */ - -importScripts('../build/router.js'); - -// Have the service worker take control as soon as possible. -self.addEventListener('install', () => self.skipWaiting()); -self.addEventListener('activate', () => self.clients.claim()); - -const routes = [ - new goog.routing.Route({ - when: ({url}) => url.pathname.endsWith('.js'), - handler: ({event}) => { - console.log('JavaScript!'); - return fetch(event.request); - }, - }), -]; - -const defaultHandler = ({event}) => { - console.log('Default!'); - return fetch(event.request); -}; - -goog.routing.registerRoutes({routes, defaultHandler}); diff --git a/packages/sw-precaching/src/lib/constants.js b/packages/sw-precaching/src/lib/constants.js index fa465f469..2e42c6b61 100644 --- a/packages/sw-precaching/src/lib/constants.js +++ b/packages/sw-precaching/src/lib/constants.js @@ -13,7 +13,7 @@ limitations under the License. */ -export const hashParamName = '_sw-precaching'; +export const cacheBustParamName = '_sw-precaching'; export const version = 'v1'; export const dbName = 'sw-precaching'; export const dbVersion = '1'; diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js index 2d5619be4..d5b72465b 100644 --- a/packages/sw-precaching/src/lib/revisioned-cache-manager.js +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -16,7 +16,7 @@ import assert from '../../../../lib/assert'; import ErrorFactory from './error-factory'; import IDBHelper from '../../../../lib/idb-helper.js'; -import {defaultCacheName, dbName, dbVersion, dbStorename} +import {defaultCacheName, dbName, dbVersion, dbStorename, cacheBustParamName} from './constants'; class RevisionedCacheManager { @@ -37,7 +37,7 @@ class RevisionedCacheManager { parsedFileEntry = { path: revisionedFileEntry, revision: revisionedFileEntry, - cacheBust: true, + cacheBust: false, }; } @@ -70,7 +70,7 @@ class RevisionedCacheManager { // Add cache bust if its not defined if (typeof parsedFileEntry.cacheBust === 'undefined') { - parsedFileEntry.cached = true; + parsedFileEntry.cacheBust = true; } else if (typeof parsedFileEntry.cacheBust !== 'boolean') { throw ErrorFactory.createError('invalid-file-manifest-entry', new Error('Invalid cacheBust: ' + @@ -129,9 +129,17 @@ class RevisionedCacheManager { } } - return openCache.add(new Request(assetAndHash.path, { + let requestUrl = assetAndHash.path; + if (assetAndHash.cacheBust) { + requestUrl = this._cacheBustUrl(requestUrl, assetAndHash.revision); + } + + return fetch(requestUrl, { credentials: 'same-origin', - })) + }) + .then((response) => { + return openCache.put(assetAndHash.path, response); + }) .then(() => { return this._putRevisionDetails( assetAndHash.path, assetAndHash.revision); @@ -162,10 +170,18 @@ class RevisionedCacheManager { return this._idbHelper.get(path); } - _putRevisionDetails (path, revision) { + _putRevisionDetails(path, revision) { return this._idbHelper.put(path, revision); } + _cacheBustUrl(url, cacheBust) { + const parsedURL = new URL(url); + parsedURL.search += (parsedURL.search ? '&' : '') + + encodeURIComponent(cacheBustParamName) + '=' + + encodeURIComponent(cacheBust); + return parsedURL.toString(); + } + _close() { this._idbHelper.close(); } diff --git a/packages/sw-precaching/test/browser-unit/cache-testing.js b/packages/sw-precaching/test/browser-unit/cache-testing.js index 3e1401299..85f13c7b1 100644 --- a/packages/sw-precaching/test/browser-unit/cache-testing.js +++ b/packages/sw-precaching/test/browser-unit/cache-testing.js @@ -2,16 +2,13 @@ describe('sw-precaching Test Revisioned Caching', function() { const deleteIndexedDB = () => { - console.log('Deleting idexedDB'); return new Promise((resolve, reject) => { // TODO: Move to constants const req = indexedDB.deleteDatabase('sw-precaching'); req.onsuccess = function() { - console.log('on success'); resolve(); }; req.onerror = function() { - console.log('on error'); reject(); }; req.onblocked = function() { @@ -86,13 +83,6 @@ describe('sw-precaching Test Revisioned Caching', function() { }; const compareCachedAssets = function(beforeData, afterData) { - /** - Object.keys(step2Responses).forEach((urlKey) => { - if (step2Responses[urlKey] === step1Responses[urlKey]) { - console.log(step2Responses[urlKey], step1Responses[urlKey]); - } - }); - */ afterData.cacheList.forEach((afterAssetAndHash) => { if (typeof assetAndHash === 'string') { afterAssetAndHash = {path: afterAssetAndHash, revision: afterAssetAndHash}; @@ -129,21 +119,16 @@ describe('sw-precaching Test Revisioned Caching', function() { }; it('should cache and fetch files', function() { - console.log('Activating service worker 1'); return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js') .then((iframe) => { - console.log('Testing service worker 1 cached set 1 step 1 assets correctly.'); return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-1']); }) .then((step1Responses) => { - console.log('Activating service worker 2'); return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js') .then((iframe) => { - console.log('Testing service worker 1 cached set 1 step 2 assets correctly.'); return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-2']); }) .then((step2Responses) => { - console.log('Comparing two file assets'); compareCachedAssets({ cacheList: goog.__TEST_DATA['set-1']['step-1'], cachedResponses: step1Responses, diff --git a/packages/sw-precaching/test/browser-unit/library-namespace.js b/packages/sw-precaching/test/browser-unit/library-namespace.js index e4e153595..eb99fb771 100644 --- a/packages/sw-precaching/test/browser-unit/library-namespace.js +++ b/packages/sw-precaching/test/browser-unit/library-namespace.js @@ -34,7 +34,6 @@ describe('Test Behaviors of Loading the Script', function() { const scriptElement = document.createElement('script'); scriptElement.src = '/packages/sw-precaching/build/sw-precaching.min.js'; scriptElement.addEventListener('error', (event) => { - console.log(event); reject(); }); document.head.appendChild(scriptElement); @@ -47,7 +46,6 @@ describe('Test Behaviors of Loading the Script', function() { ]; swUnitTests.forEach((swUnitTestPath) => { it(`should perform ${swUnitTestPath} sw tests`, function() { - console.log(swUnitTestPath); return window.goog.mochaUtils.startServiceWorkerMochaTests(swUnitTestPath) .then((testResults) => { if (testResults.failed.length > 0) { diff --git a/utils/test-server.js b/utils/test-server.js index a92f1fefa..0cf37088a 100644 --- a/utils/test-server.js +++ b/utils/test-server.js @@ -33,12 +33,12 @@ app.get('/test/iframe/:random', function(req, res) { }); app.get('/__echo/filename/:file', function(req, res) { - res.setHeader('Cache-Control', 24 * 60 * 60 * 1000); + res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); res.send(req.params.file); }); app.get('/__echo/date/:file', function(req, res) { - res.setHeader('Cache-Control', 24 * 60 * 60 * 1000); + res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); res.send(`${req.params.file}-${Date.now()}`); }); From b9fdbcca8e7b13bfa3fe699ef7f0e8b3ceed1b02 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Thu, 1 Dec 2016 16:06:56 -0800 Subject: [PATCH 12/19] Adding tests to manage cache and indexeddb deletion --- .../src/lib/revisioned-cache-manager.js | 24 +++++++++----- .../test/browser-unit/cache-testing.js | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js index d5b72465b..a635245b0 100644 --- a/packages/sw-precaching/src/lib/revisioned-cache-manager.js +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -119,14 +119,21 @@ class RevisionedCacheManager { const cachePromises = assetsAndHahes.map((assetAndHash) => { return this._getRevisionDetails(assetAndHash.path) .then((previousRevisionDetails) => { - if (previousRevisionDetails) { - if (previousRevisionDetails === assetAndHash.revision) { - /* eslint-disable no-console */ - console.log(' Already Cached? TODO: Need to check cache to ' + - 'ensure it\'s actually already cached.'); - /* eslint-enable no-console */ - return Promise.resolve(); - } + if (previousRevisionDetails === assetAndHash.revision) { + return openCache.match(assetAndHash.path) + .then((result) => { + if (result) { + return true; + } + return false; + }); + } else { + return false; + } + }) + .then((isAlreadyCached) => { + if (isAlreadyCached) { + return; } let requestUrl = assetAndHash.path; @@ -146,6 +153,7 @@ class RevisionedCacheManager { }); }); }); + return Promise.all(cachePromises) .then(() => { return openCache.keys(); diff --git a/packages/sw-precaching/test/browser-unit/cache-testing.js b/packages/sw-precaching/test/browser-unit/cache-testing.js index 85f13c7b1..3b9bd4f52 100644 --- a/packages/sw-precaching/test/browser-unit/cache-testing.js +++ b/packages/sw-precaching/test/browser-unit/cache-testing.js @@ -139,4 +139,36 @@ describe('sw-precaching Test Revisioned Caching', function() { }); }); }); + + it('should manage cache deletion', function() { + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js') + .then((iframe) => { + return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-1']); + }) + .then((step1Responses) => { + return window.goog.swUtils.clearAllCaches() + .then(() => { + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js'); + }) + .then((iframe) => { + return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-2']); + }); + }); + }); + + it('should manage indexedDB deletion', function() { + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js') + .then((iframe) => { + return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-1']); + }) + .then((step1Responses) => { + return deleteIndexedDB() + .then(() => { + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js'); + }) + .then((iframe) => { + return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-2']); + }); + }); + }); }); From 49600d260009a28387a17dc29d7781689b39ae9e Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Thu, 1 Dec 2016 17:06:08 -0800 Subject: [PATCH 13/19] Switching away from async and await due to build issues --- .../sw-precaching/src/lib/error-factory.js | 2 - .../src/lib/revisioned-cache-manager.js | 265 +++++++++++------- 2 files changed, 166 insertions(+), 101 deletions(-) diff --git a/packages/sw-precaching/src/lib/error-factory.js b/packages/sw-precaching/src/lib/error-factory.js index 42b7918de..0385964e7 100644 --- a/packages/sw-precaching/src/lib/error-factory.js +++ b/packages/sw-precaching/src/lib/error-factory.js @@ -17,8 +17,6 @@ import ErrorFactory from '../../../../lib/error-factory'; const errors = { 'not-in-sw': 'sw-precaching must be loaded in your service worker file.', - 'assets-not-an-array': 'Assets passed to \'install-handler.js\' must be ' + - 'an array', 'invalid-file-manifest-entry': `File manifest entries must be either a ` + `string with revision info in the path or an object with path and ` + `revision and parameters.`, diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js index a635245b0..f369db8e8 100644 --- a/packages/sw-precaching/src/lib/revisioned-cache-manager.js +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -19,70 +19,106 @@ import IDBHelper from '../../../../lib/idb-helper.js'; import {defaultCacheName, dbName, dbVersion, dbStorename, cacheBustParamName} from './constants'; +/** + * RevisionedCacheManager manages efficient caching of a file manifest, + * only downloading assets that have a new file revision. This module will + * also manage Cache Busting for URL's by adding a search a parameter on to + * the end of requests for assets before caching them. + */ class RevisionedCacheManager { + /** + * Constructing this object will register the require events for this + * module to work (i.e. install and activate events). + */ constructor() { this._eventsRegistered = false; - this._cachedAssets = []; + this._fileEntriesToCache = []; this._idbHelper = new IDBHelper(dbName, dbVersion, dbStorename); this._registerEvents(); } + /** + * The method expects an array of file entries in the revisionedFiles + * parameter. Each fileEntry should be either a string or an object. + * + * A string file entry should be paths / URL's that are revisions in the + * name (i.e. '/example/hello.1234.txt'). + * + * A file entry can also be an object that *must* have a 'path' and + * 'revision' parameter. The revision cannot be an empty string. With These + * entries you can prevent cacheBusting by setting a 'cacheBust' parameter + * to false. (i.e. {path: '/exmaple/hello.txt', revision: '1234'} or + * {path: '/exmaple/hello.txt', revision: '1234', cacheBust: false}) + * + * @param {object} options + * @param {array} options.revisionedFiles This should a list of + * file entries. + */ cache({revisionedFiles} = {}) { assert.isInstance({revisionedFiles}, Array); const parsedFileList = revisionedFiles.map((revisionedFileEntry) => { - let parsedFileEntry = revisionedFileEntry; - if (typeof parsedFileEntry === 'string') { - parsedFileEntry = { - path: revisionedFileEntry, - revision: revisionedFileEntry, - cacheBust: false, - }; - } + return this._validateFileEntry(revisionedFileEntry); + }); - if (!parsedFileEntry || typeof parsedFileEntry !== 'object') { - throw ErrorFactory.createError('invalid-file-manifest-entry', - new Error('Invalid file entry: ' + - JSON.stringify(revisionedFileEntry))); - } + this._fileEntriesToCache = this._fileEntriesToCache.concat(parsedFileList); + } - if (typeof parsedFileEntry.path !== 'string') { - throw ErrorFactory.createError('invalid-file-manifest-entry', - new Error('Invalid path: ' + JSON.stringify(revisionedFileEntry))); - } + _validateFileEntry(fileEntry) { + let parsedFileEntry = fileEntry; + if (typeof parsedFileEntry === 'string') { + parsedFileEntry = { + path: fileEntry, + revision: fileEntry, + cacheBust: false, + }; + } - try { - parsedFileEntry.path = - new URL(parsedFileEntry.path, location.origin).toString(); - } catch (err) { - throw ErrorFactory.createError('invalid-file-manifest-entry', - new Error('Unable to parse path as URL: ' + - JSON.stringify(revisionedFileEntry))); - } + if (!parsedFileEntry || typeof parsedFileEntry !== 'object') { + throw ErrorFactory.createError('invalid-file-manifest-entry', + new Error('Invalid file entry: ' + + JSON.stringify(fileEntry))); + } - if (typeof parsedFileEntry.revision !== 'string' || - parsedFileEntry.revision.length == 0) { - throw ErrorFactory.createError('invalid-file-manifest-entry', - new Error('Invalid revision: ' + - JSON.stringify(revisionedFileEntry))); - } + if (typeof parsedFileEntry.path !== 'string') { + throw ErrorFactory.createError('invalid-file-manifest-entry', + new Error('Invalid path: ' + JSON.stringify(fileEntry))); + } - // Add cache bust if its not defined - if (typeof parsedFileEntry.cacheBust === 'undefined') { - parsedFileEntry.cacheBust = true; - } else if (typeof parsedFileEntry.cacheBust !== 'boolean') { - throw ErrorFactory.createError('invalid-file-manifest-entry', - new Error('Invalid cacheBust: ' + - JSON.stringify(revisionedFileEntry))); - } + try { + parsedFileEntry.path = + new URL(parsedFileEntry.path, location.origin).toString(); + } catch (err) { + throw ErrorFactory.createError('invalid-file-manifest-entry', + new Error('Unable to parse path as URL: ' + + JSON.stringify(fileEntry))); + } - return parsedFileEntry; - }); + if (typeof parsedFileEntry.revision !== 'string' || + parsedFileEntry.revision.length == 0) { + throw ErrorFactory.createError('invalid-file-manifest-entry', + new Error('Invalid revision: ' + + JSON.stringify(fileEntry))); + } + + // Add cache bust if its not defined + if (typeof parsedFileEntry.cacheBust === 'undefined') { + parsedFileEntry.cacheBust = true; + } else if (typeof parsedFileEntry.cacheBust !== 'boolean') { + throw ErrorFactory.createError('invalid-file-manifest-entry', + new Error('Invalid cacheBust: ' + + JSON.stringify(fileEntry))); + } - this._cachedAssets = this._cachedAssets.concat(parsedFileList); + return parsedFileEntry; } + /** + * This method registers the service worker events. This should only + * be called once in the constructor and will do nothing if called + * multiple times. + */ _registerEvents() { if (this._eventsRegistered) { // Only need to register events once. @@ -92,101 +128,132 @@ class RevisionedCacheManager { this._eventsRegistered = true; self.addEventListener('install', (event) => { - if (this._cachedAssets.length === 0) { + if (this._fileEntriesToCache.length === 0) { return; } event.waitUntil( this._performInstallStep() - .then(() => { - this._close(); - }) ); }); } - _performInstallStep() { - const assetsAndHahes = this._cachedAssets; - const cacheId = null; - if (!Array.isArray(assetsAndHahes)) { - throw ErrorFactory.createError('assets-not-an-array'); - } - - const cacheName = cacheId || defaultCacheName; + _performInstallStep(cacheName) { + cacheName = cacheName || defaultCacheName; + let openCache; return caches.open(cacheName) - .then((openCache) => { - const cachePromises = assetsAndHahes.map((assetAndHash) => { - return this._getRevisionDetails(assetAndHash.path) - .then((previousRevisionDetails) => { - if (previousRevisionDetails === assetAndHash.revision) { - return openCache.match(assetAndHash.path) - .then((result) => { - if (result) { - return true; - } - return false; - }); - } else { - return false; - } - }) - .then((isAlreadyCached) => { - if (isAlreadyCached) { + .then((oC) => { + openCache = oC; + const cachePromises = this._fileEntriesToCache.map((fileEntry) => { + return this._isAlreadyCached(fileEntry, openCache) + .then((isCached) => { + if (isCached) { return; } - let requestUrl = assetAndHash.path; - if (assetAndHash.cacheBust) { - requestUrl = this._cacheBustUrl(requestUrl, assetAndHash.revision); - } - + let requestUrl = this._cacheBustUrl(fileEntry); return fetch(requestUrl, { credentials: 'same-origin', }) .then((response) => { - return openCache.put(assetAndHash.path, response); + return openCache.put(fileEntry.path, response); }) .then(() => { - return this._putRevisionDetails( - assetAndHash.path, assetAndHash.revision); + return this._putRevisionDetails(fileEntry.path, fileEntry.revision); }); }); }); + return Promise.all(cachePromises); + }) + .then(() => { + return openCache.keys(); + }) + .then((allCachedRequests) => { + const urlsCachedOnInstall = this._fileEntriesToCache + .map((fileEntry) => fileEntry.path); + const cacheDeletePromises = allCachedRequests.map((cachedRequest) => { + if (urlsCachedOnInstall.includes(cachedRequest.url)) { + return; + } + return openCache.delete(cachedRequest); + }); + return Promise.all(cacheDeletePromises); + }) + .then(() => { + // Closed indexedDB now that we are done with the install step + this._close(); + }); + } - return Promise.all(cachePromises) - .then(() => { - return openCache.keys(); - }) - .then((openCacheKeys) => { - const urlsCachedOnInstall = assetsAndHahes.map((assetAndHash) => { - return assetAndHash.path; - }); + /** + * This method confirms with a fileEntry is already in the cache with the + * appropriate revision. + * If the revision is known, matching the requested `fileEntry.revision` and + * the cache entry exists for the `fileEntry.path` this method returns true. + * False otherwise. + * @param {Object} fileEntry A file entry with `path` and `revision` + * parameters. + * @param {Cache} openCache The cache to look for the asset in. + * @return {Promise} Returns true is the fileEntry is already + * cached, false otherwise. + */ + _isAlreadyCached(fileEntry, openCache) { + return this._getRevisionDetails(fileEntry.path) + .then((previousRevisionDetails) => { + if (previousRevisionDetails !== fileEntry.revision) { + return false; + } - const cacheDeletePromises = openCacheKeys.map((cachedRequest) => { - if (!urlsCachedOnInstall.includes(cachedRequest.url)) { - return openCache.delete(cachedRequest); - } - return Promise.resolve(); - }); - return Promise.all(cacheDeletePromises); + return openCache.match(fileEntry.path) + .then((cacheMatch) => { + if (cacheMatch) { + return true; + } + + return false; }); }); } + /** + * This method gets the revision details for a given path. + * @param {String} path The path of an asset to look up. + * @return {Promise} Returns a string for the last revision or + * returns null if there is no revision information. + */ _getRevisionDetails(path) { return this._idbHelper.get(path); } + /** + * This method saves the revision details to indexedDB. + * @param {String} path The path for the asset. + * @param {String} revision The current revision for this asset path. + * @return {Promise} Promise that resolves once the data has been saved. + */ _putRevisionDetails(path, revision) { return this._idbHelper.put(path, revision); } - _cacheBustUrl(url, cacheBust) { - const parsedURL = new URL(url); + /** + * This method takes a file entry and if the `cacheBust` parameter is set to + * true, the cacheBust parameter will be added to the URL before making the + * request. The response will be cached with the absolute URL without + * the cache busting search param. + * @param {Object} fileEntry This is an object with `path`, `revision` and + * `cacheBust` parameters. + * @return {String} The final URL to make the request to then cache. + */ + _cacheBustUrl(fileEntry) { + if (fileEntry.cacheBust === false) { + return fileEntry.path; + } + + const parsedURL = new URL(fileEntry.path); parsedURL.search += (parsedURL.search ? '&' : '') + encodeURIComponent(cacheBustParamName) + '=' + - encodeURIComponent(cacheBust); + encodeURIComponent(fileEntry.revision); return parsedURL.toString(); } From 3b30405c892a842bb50fef53f840255dccb9b051 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Thu, 1 Dec 2016 17:10:57 -0800 Subject: [PATCH 14/19] sw-precaching --- .../src/lib/revisioned-cache-manager.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js index f369db8e8..7f7fba678 100644 --- a/packages/sw-precaching/src/lib/revisioned-cache-manager.js +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -65,6 +65,15 @@ class RevisionedCacheManager { this._fileEntriesToCache = this._fileEntriesToCache.concat(parsedFileList); } + /** + * This method ensures that the file entry in the file maniest is valid and + * if the entry is a revisioned string path, it is converted to an object + * with the desired fields. + * @param {String | object} fileEntry Either a path for a file or an object + * with a `path`, `revision` and optional `cacheBust` parameter. + * @return {object} Returns a parsed version of the file entry with absolute + * URL, revision and a cacheBust value. + */ _validateFileEntry(fileEntry) { let parsedFileEntry = fileEntry; if (typeof parsedFileEntry === 'string') { @@ -138,6 +147,13 @@ class RevisionedCacheManager { }); } + /** + * This method manages the actual install event to cache the revisioned + * assets. + * @param {String} cacheName The name to use for the cache + * @return {Promise} The promise resolves when all the desired assets are + * cached. + */ _performInstallStep(cacheName) { cacheName = cacheName || defaultCacheName; @@ -257,6 +273,9 @@ class RevisionedCacheManager { return parsedURL.toString(); } + /** + * This method closes the indexdDB helper. + */ _close() { this._idbHelper.close(); } From 92058e80afc95562bbf0cd5de743503ea375ef36 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Fri, 2 Dec 2016 09:57:27 -0800 Subject: [PATCH 15/19] Moving to async and await --- packages/sw-precaching/build.js | 16 ++-- .../src/lib/generate-precache-manifest.js | 21 ---- .../src/lib/revisioned-cache-manager.js | 96 ++++++++----------- 3 files changed, 47 insertions(+), 86 deletions(-) delete mode 100644 packages/sw-precaching/src/lib/generate-precache-manifest.js diff --git a/packages/sw-precaching/build.js b/packages/sw-precaching/build.js index 23607c9a3..9515a68af 100755 --- a/packages/sw-precaching/build.js +++ b/packages/sw-precaching/build.js @@ -27,16 +27,16 @@ module.exports = () => { format: 'umd', moduleName: 'goog.precaching', plugins: [ + rollupBabel({ + plugins: ['transform-async-to-generator', 'external-helpers'], + exclude: 'node_modules/**', + }), resolve({ jsnext: true, main: true, browser: true, }), commonjs(), - rollupBabel({ - plugins: ['transform-async-to-generator', 'external-helpers'], - exclude: 'node_modules/**', - }), ], }, outputName: pkg.main, @@ -47,16 +47,16 @@ module.exports = () => { entry: path.join(__dirname, 'src', 'index.js'), format: 'es', plugins: [ + rollupBabel({ + plugins: ['transform-async-to-generator', 'external-helpers'], + exclude: 'node_modules/**', + }), resolve({ jsnext: true, main: true, browser: true, }), commonjs(), - rollupBabel({ - plugins: ['transform-async-to-generator', 'external-helpers'], - exclude: 'node_modules/**', - }), ], }, outputName: pkg['jsnext:main'], diff --git a/packages/sw-precaching/src/lib/generate-precache-manifest.js b/packages/sw-precaching/src/lib/generate-precache-manifest.js deleted file mode 100644 index e30b1660e..000000000 --- a/packages/sw-precaching/src/lib/generate-precache-manifest.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - 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. -*/ - -import assert from '../../../../lib/assert'; - -export default (configuration) => { - const {filePatterns} = configuration; - assert.isType(filePatterns, 'array'); -}; diff --git a/packages/sw-precaching/src/lib/revisioned-cache-manager.js b/packages/sw-precaching/src/lib/revisioned-cache-manager.js index 7f7fba678..3510ffd71 100644 --- a/packages/sw-precaching/src/lib/revisioned-cache-manager.js +++ b/packages/sw-precaching/src/lib/revisioned-cache-manager.js @@ -154,52 +154,42 @@ class RevisionedCacheManager { * @return {Promise} The promise resolves when all the desired assets are * cached. */ - _performInstallStep(cacheName) { + async _performInstallStep(cacheName) { cacheName = cacheName || defaultCacheName; - let openCache; - return caches.open(cacheName) - .then((oC) => { - openCache = oC; - const cachePromises = this._fileEntriesToCache.map((fileEntry) => { - return this._isAlreadyCached(fileEntry, openCache) - .then((isCached) => { - if (isCached) { - return; - } - - let requestUrl = this._cacheBustUrl(fileEntry); - return fetch(requestUrl, { - credentials: 'same-origin', - }) - .then((response) => { - return openCache.put(fileEntry.path, response); - }) - .then(() => { - return this._putRevisionDetails(fileEntry.path, fileEntry.revision); - }); - }); - }); - return Promise.all(cachePromises); - }) - .then(() => { - return openCache.keys(); - }) - .then((allCachedRequests) => { - const urlsCachedOnInstall = this._fileEntriesToCache - .map((fileEntry) => fileEntry.path); - const cacheDeletePromises = allCachedRequests.map((cachedRequest) => { - if (urlsCachedOnInstall.includes(cachedRequest.url)) { - return; - } - return openCache.delete(cachedRequest); + let openCache = await caches.open(cacheName); + const cachePromises = this._fileEntriesToCache.map(async (fileEntry) => { + const isCached = await this._isAlreadyCached(fileEntry, openCache); + if (isCached) { + return; + } + + let requestUrl = this._cacheBustUrl(fileEntry); + const response = await fetch(requestUrl, { + credentials: 'same-origin', }); - return Promise.all(cacheDeletePromises); - }) - .then(() => { - // Closed indexedDB now that we are done with the install step - this._close(); + await openCache.put(fileEntry.path, response); + await this._putRevisionDetails(fileEntry.path, fileEntry.revision); + }); + + await Promise.all(cachePromises); + + const urlsCachedOnInstall = this._fileEntriesToCache + .map((fileEntry) => fileEntry.path); + const allCachedRequests = await openCache.keys(); + + const cacheDeletePromises = allCachedRequests.map((cachedRequest) => { + if (urlsCachedOnInstall.includes(cachedRequest.url)) { + return; + } + + return openCache.delete(cachedRequest); }); + + await Promise.all(cacheDeletePromises); + + // Closed indexedDB now that we are done with the install step + this._close(); } /** @@ -214,22 +204,14 @@ class RevisionedCacheManager { * @return {Promise} Returns true is the fileEntry is already * cached, false otherwise. */ - _isAlreadyCached(fileEntry, openCache) { - return this._getRevisionDetails(fileEntry.path) - .then((previousRevisionDetails) => { - if (previousRevisionDetails !== fileEntry.revision) { - return false; - } - - return openCache.match(fileEntry.path) - .then((cacheMatch) => { - if (cacheMatch) { - return true; - } + async _isAlreadyCached(fileEntry, openCache) { + const revisionDetails = await this._getRevisionDetails(fileEntry.path); + if (revisionDetails !== fileEntry.revision) { + return false; + } - return false; - }); - }); + const cachedResponse = await openCache.match(fileEntry.path); + return cachedResponse ? true : false; } /** From f08fe5234e47e3fdfe87feca6bb6d7af51bf73ca Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Fri, 2 Dec 2016 10:50:25 -0800 Subject: [PATCH 16/19] sw-precaching --- gulp-tasks/serve.js | 10 ++- .../test/automated-test-suite.js | 25 ++++-- .../test/browser-unit/cache-testing.js | 16 ++-- .../data/basic-cache/basic-cache-sw.js | 3 + .../test/browser-unit/data/test-data.js | 88 +++++++++++++++++-- .../{test-server.js => create-test-server.js} | 52 ++++++----- 6 files changed, 150 insertions(+), 44 deletions(-) rename utils/{test-server.js => create-test-server.js} (61%) diff --git a/gulp-tasks/serve.js b/gulp-tasks/serve.js index bb4d2760f..c08cbda04 100644 --- a/gulp-tasks/serve.js +++ b/gulp-tasks/serve.js @@ -16,11 +16,15 @@ /* eslint-disable no-console, valid-jsdoc */ const gulp = require('gulp'); -const testServer = require('../utils/test-server.js'); +const createTestServer = require('../utils/create-test-server.js'); gulp.task('serve', (unusedCallback) => { - return testServer.start('.', global.port) + return createTestServer().start('.', global.port) .then((port) => { - console.log(`Serving at http://localhost:${port}/`); + console.log(`Primary Server http://localhost:${port}/`); + return createTestServer().start('.', global.port + 1); + }) + .then((secondPort) => { + console.log(`Third Party Server http://localhost:${secondPort}/`); }); }); diff --git a/packages/sw-precaching/test/automated-test-suite.js b/packages/sw-precaching/test/automated-test-suite.js index 4b49128b1..e9799f73f 100644 --- a/packages/sw-precaching/test/automated-test-suite.js +++ b/packages/sw-precaching/test/automated-test-suite.js @@ -15,7 +15,7 @@ const seleniumAssistant = require('selenium-assistant'); const swTestingHelpers = require('sw-testing-helpers'); -const testServer = require('../../../utils/test-server.js'); +const createTestServer = require('../../../utils/create-test-server.js'); require('chromedriver'); require('operadriver'); @@ -31,17 +31,32 @@ const setupTestSuite = (assistantDriver) => { let globalDriverBrowser; let baseTestUrl; + let thirdPartyServer; + + let testServers = []; // Set up the web server before running any tests in this suite. before(function() { - return testServer.start('.').then((portNumber) => { - baseTestUrl = `http://localhost:${portNumber}/packages/sw-precaching`; + let primaryTestServer = createTestServer(); + let secondaryTestServer = createTestServer(); + testServers.push(primaryTestServer); + testServers.push(secondaryTestServer); + + return primaryTestServer.start('.') + .then((portNumber) => { + baseTestUrl = `http://localhost:${portNumber}/packages/sw-precaching`; + return secondaryTestServer.start('.'); + }) + .then((portNumber) => { + thirdPartyServer = `http://localhost:${portNumber}`; }); }); // Kill the web server once all tests are complete. after(function() { - return testServer.stop(); + return Promise.all(testServers.map((testServer) => { + return testServer.stop(); + })); }); afterEach(function() { @@ -60,7 +75,7 @@ const setupTestSuite = (assistantDriver) => { return swTestingHelpers.mochaUtils.startWebDriverMochaTests( assistantDriver.getPrettyName(), globalDriverBrowser, - `${baseTestUrl}/test/browser-unit/` + `${baseTestUrl}/test/browser-unit/?thirdPartyServer=${encodeURIComponent(thirdPartyServer)}` ); }) .then((testResults) => { diff --git a/packages/sw-precaching/test/browser-unit/cache-testing.js b/packages/sw-precaching/test/browser-unit/cache-testing.js index 3b9bd4f52..1fa024b5f 100644 --- a/packages/sw-precaching/test/browser-unit/cache-testing.js +++ b/packages/sw-precaching/test/browser-unit/cache-testing.js @@ -23,8 +23,8 @@ describe('sw-precaching Test Revisioned Caching', function() { }); afterEach(function() { - return window.goog.swUtils.cleanState() - .then(deleteIndexedDB); + /** return window.goog.swUtils.cleanState() + .then(deleteIndexedDB);**/ }); const testFileSet = (iframe, fileSet) => { @@ -119,12 +119,12 @@ describe('sw-precaching Test Revisioned Caching', function() { }; it('should cache and fetch files', function() { - return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js') + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js?' + location.search) .then((iframe) => { return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-1']); }) .then((step1Responses) => { - return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js') + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js' + location.search) .then((iframe) => { return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-2']); }) @@ -141,14 +141,14 @@ describe('sw-precaching Test Revisioned Caching', function() { }); it('should manage cache deletion', function() { - return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js') + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js' + location.search) .then((iframe) => { return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-1']); }) .then((step1Responses) => { return window.goog.swUtils.clearAllCaches() .then(() => { - return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js'); + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js' + location.search); }) .then((iframe) => { return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-2']); @@ -157,14 +157,14 @@ describe('sw-precaching Test Revisioned Caching', function() { }); it('should manage indexedDB deletion', function() { - return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js') + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw.js' + location.search) .then((iframe) => { return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-1']); }) .then((step1Responses) => { return deleteIndexedDB() .then(() => { - return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js'); + return window.goog.swUtils.activateSW('data/basic-cache/basic-cache-sw-2.js' + location.search); }) .then((iframe) => { return testFileSet(iframe, goog.__TEST_DATA['set-1']['step-2']); diff --git a/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js b/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js index bbcfc23c7..1a74f09aa 100644 --- a/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js +++ b/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js @@ -3,6 +3,9 @@ importScripts('/packages/sw-precaching/test/browser-unit/data/test-data.js'); importScripts('/packages/sw-precaching/build/sw-precaching.min.js'); importScripts('/packages/sw-precaching/test/browser-unit/data/skip-and-claim.js'); +console.log('--------------------'); +console.log(goog.__TEST_DATA['set-1']['step-1']); +console.log('--------------------'); const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); revisionedCacheManager.cache({ revisionedFiles: goog.__TEST_DATA['set-1']['step-1'], diff --git a/packages/sw-precaching/test/browser-unit/data/test-data.js b/packages/sw-precaching/test/browser-unit/data/test-data.js index 36a45f9e0..dc0d79d86 100644 --- a/packages/sw-precaching/test/browser-unit/data/test-data.js +++ b/packages/sw-precaching/test/browser-unit/data/test-data.js @@ -1,16 +1,92 @@ -const EXAMPLE_REVISIONED_FILES_SET_1_STEP_1 = [ +let EXAMPLE_REVISIONED_FILES_SET_1_STEP_1 = [ '/__echo/date/1.1234.txt', '/__echo/date/2.1234.txt', - {path: '/__echo/date/3.txt', revision: '1234'}, - {path: '/__echo/date/4.txt', revision: '1234'}, + { + path: '/__echo/date/3.txt', + revision: '1234', + }, + { + path: '/__echo/date/4.txt', + revision: '1234', + }, + new URL('/__echo/date/5.1234.txt', location.origin).toString(), + new URL('/__echo/date/6.1234.txt', location.origin).toString(), + { + path: new URL('/__echo/date/7.txt', location.origin).toString(), + revision: '1234', + }, + { + path: new URL('/__echo/date/8.txt', location.origin).toString(), + revision: '1234', + }, ]; -const EXAMPLE_REVISIONED_FILES_SET_1_STEP_2 = [ +let EXAMPLE_REVISIONED_FILES_SET_1_STEP_2 = [ '/__echo/date/1.5678.txt', '/__echo/date/2.1234.txt', - {path: '/__echo/date/3.txt', revision: '5678'}, - {path: '/__echo/date/4.txt', revision: '1234'}, + { + path: '/__echo/date/3.txt', + revision: '5678', + }, + { + path: '/__echo/date/4.txt', + revision: '1234', + }, + new URL('/__echo/date/5.5678.txt', location.origin).toString(), + new URL('/__echo/date/6.1234.txt', location.origin).toString(), + { + path: new URL('/__echo/date/7.txt', location.origin).toString(), + revision: '5678', + }, + { + path: new URL('/__echo/date/8.txt', location.origin).toString(), + revision: '1234', + }, ]; +let thirdPartyServer; +if (location.origin === 'http://localhost:3000') { + thirdPartyServer = 'http://localhost:3001'; +} else { + const getParameterByName = (name) => { + const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); + const results = regex.exec(location.href); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + + thirdPartyServer = getParameterByName('thirdPartyServer'); +} + +if (thirdPartyServer) { + const thirdPartySet1 = [ + new URL('/__echo/date-with-cors/9.1234.txt', thirdPartyServer).toString(), + new URL('/__echo/date-with-cors/10.1234.txt', thirdPartyServer).toString(), + { + path: new URL('/__echo/date-with-cors/11.txt', thirdPartyServer).toString(), + revision: '1234', + }, + { + path: new URL('/__echo/date-with-cors/12.txt', thirdPartyServer).toString(), + revision: '1234', + }, + ]; + const thirdPartySet2 = [ + new URL('/__echo/date-with-cors/9.5678.txt', thirdPartyServer).toString(), + new URL('/__echo/date-with-cors/10.1234.txt', thirdPartyServer).toString(), + { + path: new URL('/__echo/date-with-cors/11.txt', thirdPartyServer).toString(), + revision: '5678', + }, + { + path: new URL('/__echo/date-with-cors/12.txt', thirdPartyServer).toString(), + revision: '1234', + }, + ]; + EXAMPLE_REVISIONED_FILES_SET_1_STEP_1 = EXAMPLE_REVISIONED_FILES_SET_1_STEP_1.concat(thirdPartySet1); + EXAMPLE_REVISIONED_FILES_SET_1_STEP_2 = EXAMPLE_REVISIONED_FILES_SET_1_STEP_2.concat(thirdPartySet2); +} + self.goog = self.goog || {}; self.goog.__TEST_DATA = { 'set-1': { diff --git a/utils/test-server.js b/utils/create-test-server.js similarity index 61% rename from utils/test-server.js rename to utils/create-test-server.js index 0cf37088a..4f9023a33 100644 --- a/utils/test-server.js +++ b/utils/create-test-server.js @@ -42,30 +42,38 @@ app.get('/__echo/date/:file', function(req, res) { res.send(`${req.params.file}-${Date.now()}`); }); -let server; -module.exports = { - start: (rootDirectory, port) => { - if (server) { - return Promise.reject(new Error('Server already started.')); - } +app.get('/__echo/date-with-cors/:file', function(req, res) { + res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.send(`${req.params.file}-${Date.now()}`); +}); + +module.exports = () => { + let server; + return { + start: (rootDirectory, port) => { + if (server) { + return Promise.reject(new Error('Server already started.')); + } - app.use('/', express.static(rootDirectory, { - setHeaders: (res) => { - res.setHeader('Service-Worker-Allowed', '/'); - }, - })); + app.use('/', express.static(rootDirectory, { + setHeaders: (res) => { + res.setHeader('Service-Worker-Allowed', '/'); + }, + })); - app.use(serveStatic(rootDirectory)); - app.use(serveIndex(rootDirectory, {view: 'details'})); + app.use(serveStatic(rootDirectory)); + app.use(serveIndex(rootDirectory, {view: 'details'})); - return new Promise((resolve, reject) => { - server = app.listen(port, 'localhost', () => { - resolve(server.address().port); + return new Promise((resolve, reject) => { + server = app.listen(port, 'localhost', () => { + resolve(server.address().port); + }); }); - }); - }, - stop: () => { - server.close(); - server = null; - }, + }, + stop: () => { + server.close(); + server = null; + }, + }; }; From 7601ac8c6240cd26dcef0512e789fb0252f69047 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Fri, 2 Dec 2016 11:41:52 -0800 Subject: [PATCH 17/19] Adding secondary server and smartest file manifest generation --- gulp-tasks/serve.js | 9 +- .../test/automated-test-suite.js | 22 +-- .../data/basic-cache/basic-cache-sw.js | 3 - .../test/browser-unit/data/test-data.js | 127 ++++++------------ utils/create-test-server.js | 79 ----------- utils/server-instance.js | 78 +++++++++++ utils/test-server.js | 73 ++++++++++ 7 files changed, 200 insertions(+), 191 deletions(-) delete mode 100644 utils/create-test-server.js create mode 100644 utils/server-instance.js create mode 100644 utils/test-server.js diff --git a/gulp-tasks/serve.js b/gulp-tasks/serve.js index c08cbda04..7e408f0ac 100644 --- a/gulp-tasks/serve.js +++ b/gulp-tasks/serve.js @@ -16,15 +16,12 @@ /* eslint-disable no-console, valid-jsdoc */ const gulp = require('gulp'); -const createTestServer = require('../utils/create-test-server.js'); +const testServer = require('../utils/test-server.js'); gulp.task('serve', (unusedCallback) => { - return createTestServer().start('.', global.port) + return testServer.start('.', global.port) .then((port) => { console.log(`Primary Server http://localhost:${port}/`); - return createTestServer().start('.', global.port + 1); - }) - .then((secondPort) => { - console.log(`Third Party Server http://localhost:${secondPort}/`); + console.log(`Secondary Server http://localhost:${port + 1}/`); }); }); diff --git a/packages/sw-precaching/test/automated-test-suite.js b/packages/sw-precaching/test/automated-test-suite.js index e9799f73f..3933af017 100644 --- a/packages/sw-precaching/test/automated-test-suite.js +++ b/packages/sw-precaching/test/automated-test-suite.js @@ -15,7 +15,7 @@ const seleniumAssistant = require('selenium-assistant'); const swTestingHelpers = require('sw-testing-helpers'); -const createTestServer = require('../../../utils/create-test-server.js'); +const testServer = require('../../../utils/test-server.js'); require('chromedriver'); require('operadriver'); @@ -31,32 +31,18 @@ const setupTestSuite = (assistantDriver) => { let globalDriverBrowser; let baseTestUrl; - let thirdPartyServer; - - let testServers = []; // Set up the web server before running any tests in this suite. before(function() { - let primaryTestServer = createTestServer(); - let secondaryTestServer = createTestServer(); - testServers.push(primaryTestServer); - testServers.push(secondaryTestServer); - - return primaryTestServer.start('.') + return testServer.start('.') .then((portNumber) => { baseTestUrl = `http://localhost:${portNumber}/packages/sw-precaching`; - return secondaryTestServer.start('.'); - }) - .then((portNumber) => { - thirdPartyServer = `http://localhost:${portNumber}`; }); }); // Kill the web server once all tests are complete. after(function() { - return Promise.all(testServers.map((testServer) => { - return testServer.stop(); - })); + return testServer.stop(); }); afterEach(function() { @@ -75,7 +61,7 @@ const setupTestSuite = (assistantDriver) => { return swTestingHelpers.mochaUtils.startWebDriverMochaTests( assistantDriver.getPrettyName(), globalDriverBrowser, - `${baseTestUrl}/test/browser-unit/?thirdPartyServer=${encodeURIComponent(thirdPartyServer)}` + `${baseTestUrl}/test/browser-unit/` ); }) .then((testResults) => { diff --git a/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js b/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js index 1a74f09aa..bbcfc23c7 100644 --- a/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js +++ b/packages/sw-precaching/test/browser-unit/data/basic-cache/basic-cache-sw.js @@ -3,9 +3,6 @@ importScripts('/packages/sw-precaching/test/browser-unit/data/test-data.js'); importScripts('/packages/sw-precaching/build/sw-precaching.min.js'); importScripts('/packages/sw-precaching/test/browser-unit/data/skip-and-claim.js'); -console.log('--------------------'); -console.log(goog.__TEST_DATA['set-1']['step-1']); -console.log('--------------------'); const revisionedCacheManager = new goog.precaching.RevisionedCacheManager(); revisionedCacheManager.cache({ revisionedFiles: goog.__TEST_DATA['set-1']['step-1'], diff --git a/packages/sw-precaching/test/browser-unit/data/test-data.js b/packages/sw-precaching/test/browser-unit/data/test-data.js index dc0d79d86..2704ef31d 100644 --- a/packages/sw-precaching/test/browser-unit/data/test-data.js +++ b/packages/sw-precaching/test/browser-unit/data/test-data.js @@ -1,91 +1,48 @@ -let EXAMPLE_REVISIONED_FILES_SET_1_STEP_1 = [ - '/__echo/date/1.1234.txt', - '/__echo/date/2.1234.txt', - { - path: '/__echo/date/3.txt', - revision: '1234', - }, - { - path: '/__echo/date/4.txt', - revision: '1234', - }, - new URL('/__echo/date/5.1234.txt', location.origin).toString(), - new URL('/__echo/date/6.1234.txt', location.origin).toString(), - { - path: new URL('/__echo/date/7.txt', location.origin).toString(), - revision: '1234', - }, - { - path: new URL('/__echo/date/8.txt', location.origin).toString(), - revision: '1234', - }, -]; -let EXAMPLE_REVISIONED_FILES_SET_1_STEP_2 = [ - '/__echo/date/1.5678.txt', - '/__echo/date/2.1234.txt', - { - path: '/__echo/date/3.txt', - revision: '5678', - }, - { - path: '/__echo/date/4.txt', - revision: '1234', - }, - new URL('/__echo/date/5.5678.txt', location.origin).toString(), - new URL('/__echo/date/6.1234.txt', location.origin).toString(), - { - path: new URL('/__echo/date/7.txt', location.origin).toString(), - revision: '5678', - }, - { - path: new URL('/__echo/date/8.txt', location.origin).toString(), - revision: '1234', - }, -]; +let secondaryServer = `${location.protocol}//${location.hostname}:${parseInt(location.port) + 1}`; + +const EXAMPLE_REVISIONED_FILES_SET_1_STEP_1 = []; +const EXAMPLE_REVISIONED_FILES_SET_1_STEP_2 = []; + +const revision1 = ['1234', '1234']; +const revision2 = ['5678', '1234']; + +let fileIndex = 0; + +const addNewEntry = (origin) => { + let echoPath = '/__echo/date'; + if (!origin) { + origin = ''; + } + + if (origin === secondaryServer) { + echoPath = '/__echo/date-with-cors'; + } + + for (let i = 0; i < revision1.length; i++) { + EXAMPLE_REVISIONED_FILES_SET_1_STEP_1.push(`${origin}${echoPath}/${fileIndex}.${revision1[i]}.txt`); + EXAMPLE_REVISIONED_FILES_SET_1_STEP_2.push(`${origin}${echoPath}/${fileIndex}.${revision2[i]}.txt`); + fileIndex++; + + EXAMPLE_REVISIONED_FILES_SET_1_STEP_1.push({ + path: `${origin}${echoPath}/${fileIndex}.txt`, + revision: revision1[i], + }); + EXAMPLE_REVISIONED_FILES_SET_1_STEP_2.push({ + path: `${origin}${echoPath}/${fileIndex}.txt`, + revision: revision2[i], + }); + fileIndex++; + } +}; -let thirdPartyServer; -if (location.origin === 'http://localhost:3000') { - thirdPartyServer = 'http://localhost:3001'; -} else { - const getParameterByName = (name) => { - const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); - const results = regex.exec(location.href); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; +// Add entries with relative path +addNewEntry(); - thirdPartyServer = getParameterByName('thirdPartyServer'); -} +// Add entries with absolute path for this origin +addNewEntry(location.origin); -if (thirdPartyServer) { - const thirdPartySet1 = [ - new URL('/__echo/date-with-cors/9.1234.txt', thirdPartyServer).toString(), - new URL('/__echo/date-with-cors/10.1234.txt', thirdPartyServer).toString(), - { - path: new URL('/__echo/date-with-cors/11.txt', thirdPartyServer).toString(), - revision: '1234', - }, - { - path: new URL('/__echo/date-with-cors/12.txt', thirdPartyServer).toString(), - revision: '1234', - }, - ]; - const thirdPartySet2 = [ - new URL('/__echo/date-with-cors/9.5678.txt', thirdPartyServer).toString(), - new URL('/__echo/date-with-cors/10.1234.txt', thirdPartyServer).toString(), - { - path: new URL('/__echo/date-with-cors/11.txt', thirdPartyServer).toString(), - revision: '5678', - }, - { - path: new URL('/__echo/date-with-cors/12.txt', thirdPartyServer).toString(), - revision: '1234', - }, - ]; - EXAMPLE_REVISIONED_FILES_SET_1_STEP_1 = EXAMPLE_REVISIONED_FILES_SET_1_STEP_1.concat(thirdPartySet1); - EXAMPLE_REVISIONED_FILES_SET_1_STEP_2 = EXAMPLE_REVISIONED_FILES_SET_1_STEP_2.concat(thirdPartySet2); -} +// Add entries with absolute path for a foreign origin +addNewEntry(secondaryServer); self.goog = self.goog || {}; self.goog.__TEST_DATA = { diff --git a/utils/create-test-server.js b/utils/create-test-server.js deleted file mode 100644 index 4f9023a33..000000000 --- a/utils/create-test-server.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - 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. -*/ - -/** - * README: - * This test server is used in unit tests as well as - * by the 'gulp serve' task, so please make sure - * changes here work in all scenarios. - */ - -const path = require('path'); -const express = require('express'); -const serveIndex = require('serve-index'); -const serveStatic = require('serve-static'); - -const app = express(); - -// Test iframe is used by sw-testing-helpers to scope service workers -app.get('/test/iframe/:random', function(req, res) { - res.sendFile(path.join(__dirname, 'test-iframe.html')); -}); - -app.get('/__echo/filename/:file', function(req, res) { - res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); - res.send(req.params.file); -}); - -app.get('/__echo/date/:file', function(req, res) { - res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); - res.send(`${req.params.file}-${Date.now()}`); -}); - -app.get('/__echo/date-with-cors/:file', function(req, res) { - res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.send(`${req.params.file}-${Date.now()}`); -}); - -module.exports = () => { - let server; - return { - start: (rootDirectory, port) => { - if (server) { - return Promise.reject(new Error('Server already started.')); - } - - app.use('/', express.static(rootDirectory, { - setHeaders: (res) => { - res.setHeader('Service-Worker-Allowed', '/'); - }, - })); - - app.use(serveStatic(rootDirectory)); - app.use(serveIndex(rootDirectory, {view: 'details'})); - - return new Promise((resolve, reject) => { - server = app.listen(port, 'localhost', () => { - resolve(server.address().port); - }); - }); - }, - stop: () => { - server.close(); - server = null; - }, - }; -}; diff --git a/utils/server-instance.js b/utils/server-instance.js new file mode 100644 index 000000000..e6167d65a --- /dev/null +++ b/utils/server-instance.js @@ -0,0 +1,78 @@ +/* + 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. +*/ + +const path = require('path'); +const express = require('express'); +const serveIndex = require('serve-index'); +const serveStatic = require('serve-static'); + +class ServerInstance { + constructor() { + this._server = null; + + this._app = express(); + + // Test iframe is used by sw-testing-helpers to scope service workers + this._app.get('/test/iframe/:random', function(req, res) { + res.sendFile(path.join(__dirname, 'test-iframe.html')); + }); + + this._app.get('/__echo/filename/:file', function(req, res) { + res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); + res.send(req.params.file); + }); + + this._app.get('/__echo/date/:file', function(req, res) { + res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); + res.send(`${req.params.file}-${Date.now()}`); + }); + + this._app.get('/__echo/date-with-cors/:file', function(req, res) { + res.setHeader('Cache-Control', 'max-age=' + (24 * 60 * 60)); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.send(`${req.params.file}-${Date.now()}`); + }); + } + + start(rootDirectory, port) { + if (this._server) { + return Promise.reject(new Error('Server already started.')); + } + + this._app.use('/', express.static(rootDirectory, { + setHeaders: (res) => { + res.setHeader('Service-Worker-Allowed', '/'); + }, + })); + + this._app.use(serveStatic(rootDirectory)); + this._app.use(serveIndex(rootDirectory, {view: 'details'})); + + return new Promise((resolve, reject) => { + this._server = this._app.listen(port, 'localhost', () => { + resolve(this._server.address().port); + }); + }); + } + + stop() { + return new Promise((resolve) => { + this._server.close(resolve); + this._server = null; + }); + } +} + +module.exports = ServerInstance; diff --git a/utils/test-server.js b/utils/test-server.js new file mode 100644 index 000000000..13e926f4b --- /dev/null +++ b/utils/test-server.js @@ -0,0 +1,73 @@ +/* + 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. +*/ + +/** + * README: + * This test server is used in unit tests as well as + * by the 'gulp serve' task, so please make sure + * changes here work in all scenarios. + * + * There are two servers so tests can make requests to a foreign origin. + */ + +const ServerInstance = require('./server-instance'); + +let primaryServer; +let secondaryServer; + +module.exports = { + /** + * Calling start will create two servers. The primary port will be returned + * and the secondary server will be on the primary port + 1; + */ + start: (rootDirectory, port) => { + if (primaryServer || secondaryServer) { + return Promise.reject('Server already running'); + } + + primaryServer = new ServerInstance(); + secondaryServer = new ServerInstance(); + + return primaryServer.start(rootDirectory, port) + .then((primaryPort) => { + let secondaryPort = primaryPort + 1; + return secondaryServer.start(rootDirectory, secondaryPort) + .then((actualSecondaryPort) => { + if (actualSecondaryPort !== secondaryPort) { + return Promise.all([ + primaryServer.stop(), + secondaryServer.stop(), + ]) + .then(() => { + Promise.reject(new Error('Server could not get the required ' + + 'ports or primaryPort and primaryPort + 1.')); + }); + } else { + return primaryPort; + } + }); + }); + }, + stop: () => { + return Promise.all([ + primaryServer.stop(), + secondaryServer.stop(), + ]) + .then(() => { + primaryServer = null; + secondaryServer = null; + }); + }, +}; From 68d8f9be1f7be4d47f0ef2945b30fd8465940112 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Fri, 2 Dec 2016 11:54:29 -0800 Subject: [PATCH 18/19] Hopefully making travis happy --- .../test/automated-test-suite.js | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/sw-precaching/test/automated-test-suite.js b/packages/sw-precaching/test/automated-test-suite.js index 3933af017..71aedb5fd 100644 --- a/packages/sw-precaching/test/automated-test-suite.js +++ b/packages/sw-precaching/test/automated-test-suite.js @@ -24,35 +24,35 @@ require('geckodriver'); const RETRIES = 4; const TIMEOUT = 10 * 1000; -const setupTestSuite = (assistantDriver) => { - describe(`sw-praching Tests in ${assistantDriver.getPrettyName()}`, function() { - this.retries(RETRIES); - this.timeout(TIMEOUT); +describe(`sw-precaching Browser Tests`, function() { + this.retries(RETRIES); + this.timeout(TIMEOUT); - let globalDriverBrowser; - let baseTestUrl; + let globalDriverBrowser; + let baseTestUrl; - // Set up the web server before running any tests in this suite. - before(function() { - return testServer.start('.') - .then((portNumber) => { - baseTestUrl = `http://localhost:${portNumber}/packages/sw-precaching`; - }); + // Set up the web server before running any tests in this suite. + before(function() { + return testServer.start('.') + .then((portNumber) => { + baseTestUrl = `http://localhost:${portNumber}/packages/sw-precaching`; }); + }); - // Kill the web server once all tests are complete. - after(function() { - return testServer.stop(); - }); + // Kill the web server once all tests are complete. + after(function() { + return testServer.stop(); + }); - afterEach(function() { - return seleniumAssistant.killWebDriver(globalDriverBrowser) - .then(() => { - globalDriverBrowser = null; - }); + afterEach(function() { + return seleniumAssistant.killWebDriver(globalDriverBrowser) + .then(() => { + globalDriverBrowser = null; }); + }); - it('should pass all browser based unit tests', function() { + const setupTestSuite = (assistantDriver) => { + it(`should pass all browser based unit tests in ${assistantDriver.getPrettyName()}`, function() { return assistantDriver.getSeleniumDriver() .then((driver) => { globalDriverBrowser = driver; @@ -75,24 +75,24 @@ const setupTestSuite = (assistantDriver) => { } }); }); - }); -}; + }; -const availableBrowsers = seleniumAssistant.getAvailableBrowsers(); -availableBrowsers.forEach((browser) => { - switch(browser.getSeleniumBrowserId()) { - case 'chrome': - case 'firefox': - case 'opera': - if (browser.getSeleniumBrowserId() === 'opera' && - browser.getVersionNumber() <= 43) { - console.log(`Skipping Opera <= 43 due to driver issues.`); - return; - } - setupTestSuite(browser); - break; - default: - console.log(`Skipping tests for ${browser.getSeleniumBrowserId()}`); - break; - } + const availableBrowsers = seleniumAssistant.getAvailableBrowsers(); + availableBrowsers.forEach((browser) => { + switch(browser.getSeleniumBrowserId()) { + case 'chrome': + case 'firefox': + case 'opera': + if (browser.getSeleniumBrowserId() === 'opera' && + browser.getVersionNumber() <= 43) { + console.log(`Skipping Opera <= 43 due to driver issues.`); + return; + } + setupTestSuite(browser); + break; + default: + console.log(`Skipping tests for ${browser.getSeleniumBrowserId()}`); + break; + } + }); }); From 0d6e499640e8cc7216cca01312350d926cdb183a Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Fri, 2 Dec 2016 12:03:36 -0800 Subject: [PATCH 19/19] Fixing same server issue in sw-offline-analytics --- .../test/automated-suite.js | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/sw-offline-google-analytics/test/automated-suite.js b/packages/sw-offline-google-analytics/test/automated-suite.js index c78b4c099..3af47bd48 100644 --- a/packages/sw-offline-google-analytics/test/automated-suite.js +++ b/packages/sw-offline-google-analytics/test/automated-suite.js @@ -34,36 +34,36 @@ require('operadriver'); const TIMEOUT = 10 * 1000; const RETRIES = 3; -const configureTestSuite = function(browser) { +describe(`sw-offline-google-analytics Test Suite`, function() { + this.retries(RETRIES); + this.timeout(TIMEOUT); + let globalDriverReference = null; let baseTestUrl; - describe(`sw-offline-google-analytics Test Suite with (${browser.getPrettyName()} - ${browser.getVersionNumber()})`, function() { - this.retries(RETRIES); - this.timeout(TIMEOUT); - - // Set up the web server before running any tests in this suite. - before(function() { - return testServer.start('.').then((portNumber) => { - baseTestUrl = `http://localhost:${portNumber}/packages/sw-offline-google-analytics/test/`; - }); + // Set up the web server before running any tests in this suite. + before(function() { + return testServer.start('.').then((portNumber) => { + baseTestUrl = `http://localhost:${portNumber}/packages/sw-offline-google-analytics/test/`; }); + }); - // Kill the web server once all tests are complete. - after(function() { - return testServer.stop(); - }); + // Kill the web server once all tests are complete. + after(function() { + return testServer.stop(); + }); - afterEach(function() { - this.timeout(6000); + afterEach(function() { + this.timeout(6000); - return seleniumAssistant.killWebDriver(globalDriverReference) - .then(() => { - globalDriverReference = null; - }); + return seleniumAssistant.killWebDriver(globalDriverReference) + .then(() => { + globalDriverReference = null; }); + }); - it('should pass all tests', function() { + const configureTestSuite = function(browser) { + it(`should pass all tests in (${browser.getPrettyName()} - ${browser.getVersionNumber()})`, function() { return browser.getSeleniumDriver() .then((driver) => { globalDriverReference = driver; @@ -87,29 +87,29 @@ const configureTestSuite = function(browser) { } }); }); + }; + + seleniumAssistant.getAvailableBrowsers().forEach(function(browser) { + // Blackliist browsers here if needed. + if (browser.getSeleniumBrowserId() === 'opera' && browser.getVersionNumber() === 41) { + console.warn('Skipping Opera version 41 due to operadriver error.'); + return; + } + + switch (browser.getSeleniumBrowserId()) { + case 'chrome': + case 'firefox': + case 'opera': + if (browser.getSeleniumBrowserId() === 'opera' && + browser.getVersionNumber() <= 43) { + console.log(`Skipping Opera <= 43 due to driver issues.`); + return; + } + configureTestSuite(browser); + break; + default: + console.warn(`Skipping ${browser.getPrettyName()}.`); + break; + } }); -}; - -seleniumAssistant.getAvailableBrowsers().forEach(function(browser) { - // Blackliist browsers here if needed. - if (browser.getSeleniumBrowserId() === 'opera' && browser.getVersionNumber() === 41) { - console.warn('Skipping Opera version 41 due to operadriver error.'); - return; - } - - switch (browser.getSeleniumBrowserId()) { - case 'chrome': - case 'firefox': - case 'opera': - if (browser.getSeleniumBrowserId() === 'opera' && - browser.getVersionNumber() <= 43) { - console.log(`Skipping Opera <= 43 due to driver issues.`); - return; - } - configureTestSuite(browser); - break; - default: - console.warn(`Skipping ${browser.getPrettyName()}.`); - break; - } });