From 5fc46f020ab08dbd77de93aa83ff5fcc378a25cb Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 26 Sep 2019 22:49:35 +0900 Subject: [PATCH] Add a check to wait for the resulting client ID (#2210) * Add a check to wait for the resulting client ID * Fix some tests. * Explicitly switch windows. * Explicitly fail if we can't get a window * Add additional debug logging to test * Removing debugging code * Skip flaky test in Safari --- .../server/templates/integration.html.njk | 20 ++ .../testing/webdriver/getLastWindowHandle.js | 37 +++- infra/testing/webdriver/windowLoaded.js | 13 +- package-lock.json | 72 +++++-- .../src/BroadcastCacheUpdate.ts | 30 +++ packages/workbox-core/src/_private.ts | 4 + .../src/_private/resultingClientExists.ts | 61 ++++++ packages/workbox-core/src/_private/timeout.ts | 22 +++ .../integration/test-all.js | 175 ++++++++++++++++-- .../static/index.html | 5 +- test/workbox-broadcast-update/static/sw.js | 19 +- .../_private/test-resultingClientExists.mjs | 55 ++++++ .../workbox-core/sw/_private/test-timeout.mjs | 39 ++++ test/workbox-window/integration/test-all.js | 9 +- test/workbox-window/static/sw-window-ready.js | 24 --- test/workbox-window/window/test-Workbox.mjs | 17 +- 16 files changed, 522 insertions(+), 80 deletions(-) create mode 100644 infra/testing/server/templates/integration.html.njk create mode 100644 packages/workbox-core/src/_private/resultingClientExists.ts create mode 100644 packages/workbox-core/src/_private/timeout.ts create mode 100644 test/workbox-core/sw/_private/test-resultingClientExists.mjs create mode 100644 test/workbox-core/sw/_private/test-timeout.mjs delete mode 100644 test/workbox-window/static/sw-window-ready.js diff --git a/infra/testing/server/templates/integration.html.njk b/infra/testing/server/templates/integration.html.njk new file mode 100644 index 000000000..541125138 --- /dev/null +++ b/infra/testing/server/templates/integration.html.njk @@ -0,0 +1,20 @@ + + + + + + {{ title }} + + + + {{ body }} + + diff --git a/infra/testing/webdriver/getLastWindowHandle.js b/infra/testing/webdriver/getLastWindowHandle.js index 849da9d33..820cea931 100644 --- a/infra/testing/webdriver/getLastWindowHandle.js +++ b/infra/testing/webdriver/getLastWindowHandle.js @@ -8,16 +8,45 @@ // Store local references of these globals. -const {webdriver} = global.__workbox; +const {server, webdriver} = global.__workbox; + +const testServerOrigin = server.getAddress(); /** - * Gets the window handle of the last openned tab. + * Gets the window handle of the last opened tab. * * @return {string} */ const getLastWindowHandle = async () => { - const allHandles = await webdriver.getAllWindowHandles(); - return allHandles[allHandles.length - 1]; + let lastWindowHandle; + + // Save the handle so that we can switch back before returning. + const currentWindowHandle = await webdriver.getWindowHandle(); + + const allWindowHandles = await webdriver.getAllWindowHandles(); + // reverse() the list so that we will iterate through the last one first. + allWindowHandles.reverse(); + + for (const handle of allWindowHandles) { + await webdriver.switchTo().window(handle); + const currentUrl = await webdriver.getCurrentUrl(); + if (currentUrl.startsWith(testServerOrigin)) { + lastWindowHandle = handle; + break; + } else { + // Used for debugging failing tests with unexpected windows openning. + // eslint-disable-next-line no-console + console.log(`Unexpected window opened: ${currentUrl}`); + } + } + + await webdriver.switchTo().window(currentWindowHandle); + if (lastWindowHandle) { + return lastWindowHandle; + } + + // If we can't find anything, treat that as a fatal error. + throw new Error(`Unable to a window with origin ${testServerOrigin}.`); }; module.exports = {getLastWindowHandle}; diff --git a/infra/testing/webdriver/windowLoaded.js b/infra/testing/webdriver/windowLoaded.js index d8250ff56..0a37ded10 100644 --- a/infra/testing/webdriver/windowLoaded.js +++ b/infra/testing/webdriver/windowLoaded.js @@ -15,11 +15,20 @@ const {executeAsyncAndCatch} = require('./executeAsyncAndCatch'); const windowLoaded = async () => { // Wait for the window to load, so the `Workbox` global is available. await executeAsyncAndCatch(async (cb) => { + const loaded = () => { + if (!window.Workbox) { + const error = new Error('Workbox not yet loaded...'); + cb({error: error.stack}); + } else { + cb(); + } + }; + try { if (document.readyState === 'complete') { - cb(); + loaded(); } else { - addEventListener('load', () => cb()); + addEventListener('load', () => loaded()); } } catch (error) { cb({error: error.stack}); diff --git a/package-lock.json b/package-lock.json index d184465f0..9f11dae85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1131,6 +1131,7 @@ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, + "optional": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -1143,6 +1144,7 @@ "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", "dev": true, + "optional": true, "requires": { "dot-prop": "^4.1.0", "graceful-fs": "^4.1.2", @@ -1157,6 +1159,7 @@ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.3.tgz", "integrity": "sha512-MSmczZctbz91AxCvqp9GHBoZOSbJKAICV7Ow/AIWSJZRrRchUd5NL1b2P4OfP+4m490BEUPhhARfpHdqCxuCvg==", "dev": true, + "optional": true, "requires": { "axios": "^0.18.0", "extend": "^3.0.1", @@ -1182,6 +1185,7 @@ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-1.6.1.tgz", "integrity": "sha512-jYiWC8NA9n9OtQM7ANn0Tk464do9yhKEtaJ72pKcaBiEwn4LwcGYIYOfwtfsSm3aur/ed3tlSxbmg24IAT6gAg==", "dev": true, + "optional": true, "requires": { "axios": "^0.18.0", "gcp-metadata": "^0.6.3", @@ -1197,6 +1201,7 @@ "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.10.1.tgz", "integrity": "sha512-iIqSbY7Ypd32mnHGbYctp80vZzXoDlvI9gEfvtl3kmyy5HzOcrZCIGCBdSlIzRsg7nHpQiHE3Zl6Ycur6TSodQ==", "dev": true, + "optional": true, "requires": { "async": "^2.3.0", "gcp-metadata": "^0.6.1", @@ -1216,6 +1221,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, + "optional": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -1275,6 +1281,7 @@ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "optional": true, "requires": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" @@ -1329,7 +1336,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "dev": true, + "optional": true }, "yargs": { "version": "11.0.0", @@ -3898,6 +3906,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", "dev": true, + "optional": true, "requires": { "follow-redirects": "1.5.10", "is-buffer": "^2.0.2" @@ -5073,7 +5082,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true + "dev": true, + "optional": true }, "columnify": { "version": "1.5.4", @@ -7833,6 +7843,7 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", "dev": true, + "optional": true, "requires": { "debug": "=3.1.0" }, @@ -7842,6 +7853,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, + "optional": true, "requires": { "ms": "2.0.0" } @@ -7850,7 +7862,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "dev": true, + "optional": true } } }, @@ -8014,7 +8027,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -8035,12 +8049,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8055,17 +8071,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -8182,7 +8201,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -8194,6 +8214,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8208,6 +8229,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8215,12 +8237,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -8239,6 +8263,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -8319,7 +8344,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -8331,6 +8357,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -8416,7 +8443,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -8452,6 +8480,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8471,6 +8500,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8514,12 +8544,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -10485,7 +10517,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true + "dev": true, + "optional": true }, "is-callable": { "version": "1.1.4", @@ -12200,7 +12233,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true + "dev": true, + "optional": true }, "minimalistic-assert": { "version": "1.0.1", @@ -15153,7 +15187,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true + "dev": true, + "optional": true }, "pretty-error": { "version": "2.1.1", @@ -16050,7 +16085,8 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ==", - "dev": true + "dev": true, + "optional": true }, "retry-request": { "version": "4.1.1", diff --git a/packages/workbox-broadcast-update/src/BroadcastCacheUpdate.ts b/packages/workbox-broadcast-update/src/BroadcastCacheUpdate.ts index 4ac10de9a..42c45e606 100644 --- a/packages/workbox-broadcast-update/src/BroadcastCacheUpdate.ts +++ b/packages/workbox-broadcast-update/src/BroadcastCacheUpdate.ts @@ -7,6 +7,8 @@ */ import {assert} from 'workbox-core/_private/assert.js'; +import {timeout} from 'workbox-core/_private/timeout.js'; +import {resultingClientExists} from 'workbox-core/_private/resultingClientExists.js'; import {CacheDidUpdateCallbackParam} from 'workbox-core/types.js'; import {logger} from 'workbox-core/_private/logger.js'; import {responsesAreSame} from './responsesAreSame.js'; @@ -15,6 +17,12 @@ import {CACHE_UPDATED_MESSAGE_TYPE, CACHE_UPDATED_MESSAGE_META, DEFAULT_HEADERS_ import './_version.js'; +// UA-sniff Safari: https://stackoverflow.com/questions/7944460/detect-safari-browser +// TODO(philipwalton): remove once this Safari bug fix has been released. +// https://bugs.webkit.org/show_bug.cgi?id=201169 +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + // Give TypeScript the correct global. declare var self: ServiceWorkerGlobalScope; @@ -140,6 +148,28 @@ class BroadcastCacheUpdate { payload: this._generatePayload(options), }; + // For navigation requests, wait until the new window client exists + // before sending the message + if (options.request.mode === 'navigate') { + const resultingClientId = + options.event && options.event.resultingClientId; + + const resultingWin = await resultingClientExists(resultingClientId); + + // Safari does not currently implement postMessage buffering and + // there's no good way to feature detect that, so to increase the + // chances of the message being delivered in Safari, we add a timeout. + // We also do this if `resultingClientExists()` didn't return a client, + // which means it timed out, so it's worth waiting a bit longer. + if (!resultingWin || isSafari) { + // 3500 is chosen because (according to CrUX data) 80% of mobile + // websites hit the DOMContentLoaded event in less than 3.5 seconds. + // And presumably sites implementing service worker are on the + // higher end of the performance spectrum. + await timeout(3500); + } + } + const windows = await self.clients.matchAll({type: 'window'}); for (const win of windows) { win.postMessage(messageData); diff --git a/packages/workbox-core/src/_private.ts b/packages/workbox-core/src/_private.ts index f2d75a38a..e272b5b31 100644 --- a/packages/workbox-core/src/_private.ts +++ b/packages/workbox-core/src/_private.ts @@ -19,6 +19,8 @@ import {executeQuotaErrorCallbacks} from './_private/executeQuotaErrorCallbacks. import {fetchWrapper} from './_private/fetchWrapper.js'; import {getFriendlyURL} from './_private/getFriendlyURL.js'; import {logger} from './_private/logger.js'; +import {resultingClientExists} from './_private/resultingClientExists.js'; +import {timeout} from './_private/timeout.js'; import {WorkboxError} from './_private/WorkboxError.js'; import './_version.js'; @@ -36,5 +38,7 @@ export { fetchWrapper, getFriendlyURL, logger, + resultingClientExists, + timeout, WorkboxError, }; diff --git a/packages/workbox-core/src/_private/resultingClientExists.ts b/packages/workbox-core/src/_private/resultingClientExists.ts new file mode 100644 index 000000000..5ea63e75f --- /dev/null +++ b/packages/workbox-core/src/_private/resultingClientExists.ts @@ -0,0 +1,61 @@ +/* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +import {timeout} from './timeout.js'; +import '../_version.js'; + + +// Give TypeScript the correct global. +declare var self: ServiceWorkerGlobalScope; + +const MAX_RETRY_TIME = 2000; + +/** + * Returns a promise that resolves to a window client matching the passed + * `resultingClientId`. For browsers that don't support `resultingClientId` + * or if waiting for the resulting client to apper takes too long, resolve to + * `undefined`. + * + * @param {string} [resultingClientId] + * @return {Promise} + * @private + */ +export async function resultingClientExists(resultingClientId?: string): Promise { + if (!resultingClientId) { + return; + } + + let existingWindows = await self.clients.matchAll({type: 'window'}); + const existingWindowIds = new Set(existingWindows.map((w) => w.id)); + + let resultingWindow; + const startTime = performance.now(); + + // Only wait up to `MAX_RETRY_TIME` to find a matching client. + while (performance.now() - startTime < MAX_RETRY_TIME) { + existingWindows = await self.clients.matchAll({type: 'window'}); + + resultingWindow = existingWindows.find((w) => { + if (resultingClientId) { + // If we have a `resultingClientId`, we can match on that. + return w.id === resultingClientId + } else { + // Otherwise match on finding a window not in `existingWindowIds`. + return !existingWindowIds.has(w.id) + } + }); + + if (resultingWindow) { + break; + } + + // Sleep for 100ms and retry. + await timeout(100); + } + + return resultingWindow; +} diff --git a/packages/workbox-core/src/_private/timeout.ts b/packages/workbox-core/src/_private/timeout.ts new file mode 100644 index 000000000..49691c460 --- /dev/null +++ b/packages/workbox-core/src/_private/timeout.ts @@ -0,0 +1,22 @@ +/* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +import '../_version.js'; + + +/** + * Returns a promise that resolves and the passed number of milliseconds. + * This utily is an async/await-friendly version of `setTimeout`. + * + * @param {number} ms + * @return {Promise} + * @private + */ + +export function timeout(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/test/workbox-broadcast-update/integration/test-all.js b/test/workbox-broadcast-update/integration/test-all.js index dac36b03e..0ce2fd34f 100644 --- a/test/workbox-broadcast-update/integration/test-all.js +++ b/test/workbox-broadcast-update/integration/test-all.js @@ -9,6 +9,9 @@ const expect = require('chai').expect; const activateAndControlSW = require('../../../infra/testing/activate-and-control'); const {runUnitTests} = require('../../../infra/testing/webdriver/runUnitTests'); +const {openNewTab} = require('../../../infra/testing/webdriver/openNewTab'); +const {getLastWindowHandle} = require('../../../infra/testing/webdriver/getLastWindowHandle'); +const templateData = require('../../../infra/testing/server/template-data'); // Store local references of these globals. @@ -26,41 +29,181 @@ describe(`[workbox-broadcast-update] Plugin`, function() { const swURL = `${testingURL}sw.js`; const apiURL = `${testServerAddress}/__WORKBOX/uniqueETag`; - it(`should broadcast a message on the expected channel when there's a cache update`, async function() { + it(`should broadcast a message when there's a cache update to a regular request`, async function() { await webdriver.get(testingURL); await activateAndControlSW(swURL); + await clearAllCaches(); - const err = await webdriver.executeAsyncScript((apiURL, cb) => { - navigator.serviceWorker.addEventListener('message', (event) => { - window.__test.message = event.data; - }); - - // There's already a cached entry for apiURL created by the - // service worker's install handler. - fetch(apiURL) - .then(() => cb()) - .catch((err) => cb(err.message)); + // Fetch `apiURL`, which should put it in the cache (but not trigger an update) + const err1 = await webdriver.executeAsyncScript((apiURL, cb) => { + fetch(apiURL).then(() => cb()).catch((err) => cb(err.message)); }, apiURL); + expect(err1).to.not.exist; - expect(err).to.not.exist; + // Fetch `apiURL` again, which should trigger an update message. + const err2 = await webdriver.executeAsyncScript((apiURL, cb) => { + fetch(apiURL).then(() => cb()).catch((err) => cb(err.message)); + }, apiURL); + expect(err2).to.not.exist; await webdriver.wait(() => { return webdriver.executeScript(() => { - return typeof window.__test.message !== 'undefined'; + return window.__messages.length > 0; }); }); - const updateMessageEventData = await webdriver.executeScript(() => { - return window.__test.message; + const messages = await webdriver.executeScript(() => { + return window.__messages; }); - expect(updateMessageEventData).to.deep.equal({ + expect(messages.length).to.equal(1); + expect(messages[0]).to.deep.equal({ + type: 'CACHE_UPDATED', meta: 'workbox-broadcast-update', payload: { cacheName: 'bcu-integration-test', updatedURL: apiURL, }, + }); + }); + + it(`should broadcast a message when there's a cache update to a navigation request`, async function() { + await webdriver.get(testingURL); + await activateAndControlSW(swURL); + await clearAllCaches(); + + templateData.assign({ + title: 'Broadcast Cache Update Test', + body: '', + script: ` + window.__messages = []; + navigator.serviceWorker.addEventListener('message', (event) => { + window.__messages.push(event.data); + }); + `, + }); + + const dynamicPageURL = testingURL + 'integration.html.njk'; + + // Navigate to a dynamic page whose content can be updated from with this + // test, and wait until the cache is populated. + await webdriver.get(dynamicPageURL); + await webdriver.wait(async () => { + return webdriver.executeAsyncScript(async (url, cb) => { + cb(await caches.match(url)); + }, dynamicPageURL); + }); + + // Update the template data with new content, + // then refresh and wait until the udpate message is received. + templateData.assign({ + body: 'New content to change Content-Length!', + }); + + await webdriver.get(webdriver.getCurrentUrl()); + + await webdriver.wait(() => { + return webdriver.executeScript(() => { + return window.__messages.length > 0; + }); + }); + + const messages = await webdriver.executeScript(() => { + return window.__messages; + }); + + expect(messages.length).to.equal(1); + expect(messages[0]).to.deep.equal({ + type: 'CACHE_UPDATED', + meta: 'workbox-broadcast-update', + payload: { + cacheName: 'bcu-integration-test', + updatedURL: dynamicPageURL, + }, + }); + }); + + it(`should broadcast a message to all open window clients`, async function() { + await webdriver.get(testingURL); + await activateAndControlSW(swURL); + await clearAllCaches(); + + templateData.assign({ + title: 'Broadcast Cache Update Test', + body: '', + script: ` + window.__messages = []; + navigator.serviceWorker.addEventListener('message', (event) => { + window.__messages.push(event.data); + }); + `, + }); + + const dynamicPageURL = testingURL + 'integration.html.njk'; + + // Navigate to a dynamic page whose content can be updated from with this + // test, and wait until the cache is populated. + await webdriver.get(dynamicPageURL); + const tab1Handle = await getLastWindowHandle(); + await webdriver.wait(async () => { + return webdriver.executeAsyncScript(async (url, cb) => { + cb(await caches.match(url)); + }, dynamicPageURL); + }); + + // Update the template data with new content, + // then open a new tab and wait until the udpate message is received. + templateData.assign({ + body: 'New content to change Content-Length!', + }); + await openNewTab(dynamicPageURL); + await webdriver.wait(() => { + return webdriver.executeScript(() => { + return window.__messages.length > 0; + }); + }); + + const tab2Messsages = await webdriver.executeScript(() => { + return window.__messages; + }); + + expect(tab2Messsages.length).to.equal(1); + expect(tab2Messsages[0]).to.deep.equal({ + type: 'CACHE_UPDATED', + meta: 'workbox-broadcast-update', + payload: { + cacheName: 'bcu-integration-test', + updatedURL: dynamicPageURL, + }, + }); + + // Also assert a message was received on the first tab. + await webdriver.switchTo().window(tab1Handle); + const tab1Messsages = await webdriver.executeScript(() => { + return window.__messages; + }); + + expect(tab1Messsages.length).to.equal(1); + expect(tab1Messsages[0]).to.deep.equal({ type: 'CACHE_UPDATED', + meta: 'workbox-broadcast-update', + payload: { + cacheName: 'bcu-integration-test', + updatedURL: dynamicPageURL, + }, }); }); }); + +/** + * Clears all caches for the origin of the currently open page. + */ +async function clearAllCaches() { + await webdriver.executeAsyncScript(async (cb) => { + const cacheNames = await caches.keys(); + for (const name of cacheNames) { + await caches.delete(name); + } + cb(); + }); +} diff --git a/test/workbox-broadcast-update/static/index.html b/test/workbox-broadcast-update/static/index.html index 0ea8cb448..8a9c47e94 100644 --- a/test/workbox-broadcast-update/static/index.html +++ b/test/workbox-broadcast-update/static/index.html @@ -4,7 +4,10 @@

You need to manually register sw.js

diff --git a/test/workbox-broadcast-update/static/sw.js b/test/workbox-broadcast-update/static/sw.js index 353b1562a..6eac25266 100644 --- a/test/workbox-broadcast-update/static/sw.js +++ b/test/workbox-broadcast-update/static/sw.js @@ -15,7 +15,7 @@ const cacheName = 'bcu-integration-test'; workbox.routing.registerRoute( new RegExp('/__WORKBOX/uniqueETag$'), - workbox.strategies.staleWhileRevalidate({ + new workbox.strategies.StaleWhileRevalidate({ cacheName, plugins: [ new workbox.broadcastUpdate.BroadcastUpdatePlugin(), @@ -23,10 +23,15 @@ workbox.routing.registerRoute( }) ); -self.addEventListener('install', (event) => { - // Pre-populate the cache. - event.waitUntil(caches.open(cacheName) - .then((cache) => cache.add('/__WORKBOX/uniqueETag'))); - self.skipWaiting(); -}); +workbox.routing.registerRoute( + ({request}) => request.mode === 'navigate', + new workbox.strategies.StaleWhileRevalidate({ + cacheName, + plugins: [ + new workbox.broadcastUpdate.BroadcastUpdatePlugin(), + ], + }) +); + +self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', () => self.clients.claim()); diff --git a/test/workbox-core/sw/_private/test-resultingClientExists.mjs b/test/workbox-core/sw/_private/test-resultingClientExists.mjs new file mode 100644 index 000000000..a65ffaaff --- /dev/null +++ b/test/workbox-core/sw/_private/test-resultingClientExists.mjs @@ -0,0 +1,55 @@ +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +import {resultingClientExists} from 'workbox-core/_private/resultingClientExists.mjs'; + + +describe(`resultingClientExists()`, function() { + const sandbox = sinon.createSandbox(); + + beforeEach(async function() { + sandbox.restore(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it(`should resolve to a matching window client ID`, async function() { + sandbox.stub(clients, 'matchAll') + .onFirstCall().resolves([{id: '1'}, {id: '2'}]) + .onSecondCall().resolves([{id: '1'}, {id: '2'}]) + .onThirdCall().resolves([{id: '1'}, {id: '3'}]); + + const win = await resultingClientExists('3'); + expect(win.id).to.equal('3'); + }); + + it(`should resolve to undefined when not passed a value`, async function() { + sandbox.stub(clients, 'matchAll') + .onFirstCall().resolves([{id: '1'}, {id: '2'}]) + .onSecondCall().resolves([{id: '1'}, {id: '2'}]) + .onThirdCall().resolves([{id: '1'}, {id: '3'}]); + + const startTime = performance.now(); + const win = await resultingClientExists(); + + expect(win).to.equal(undefined); + expect(performance.now() - startTime).to.be.below(2000); + }); + + it(`should resolve to undefined after 2 seconds of unsuccessful retrying`, async function() { + sandbox.stub(clients, 'matchAll').resolves([{id: '1'}, {id: '2'}]); + + const startTime = performance.now(); + const win = await resultingClientExists('3'); + + expect(win).to.equal(undefined); + expect(performance.now() - startTime).to.be.above(2000); + }); +}); diff --git a/test/workbox-core/sw/_private/test-timeout.mjs b/test/workbox-core/sw/_private/test-timeout.mjs new file mode 100644 index 000000000..8fed21a7a --- /dev/null +++ b/test/workbox-core/sw/_private/test-timeout.mjs @@ -0,0 +1,39 @@ +/* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +import {timeout} from 'workbox-core/_private/timeout.mjs'; + + +describe(`timeout()`, function() { + const sandbox = sinon.createSandbox(); + + beforeEach(async function() { + sandbox.restore(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it(`should return a promise that resolves after the passed number of milliseconds`, function(done) { + const clock = sandbox.useFakeTimers(); + const startTime = performance.now(); + + timeout(123).then(() => { + expect(performance.now() - startTime).to.equal(123); + clock.tick(456); + }); + + timeout(456).then(() => { + expect(performance.now() - startTime).to.equal(123 + 456); + done(); + }); + + clock.tick(123); + }); +}); diff --git a/test/workbox-window/integration/test-all.js b/test/workbox-window/integration/test-all.js index ba09558e5..330121d3a 100644 --- a/test/workbox-window/integration/test-all.js +++ b/test/workbox-window/integration/test-all.js @@ -17,7 +17,7 @@ const {windowLoaded} = require('../../../infra/testing/webdriver/windowLoaded'); // Store local references of these globals. -const {webdriver, server} = global.__workbox; +const {webdriver, server, seleniumBrowser} = global.__workbox; const testServerOrigin = server.getAddress(); const testPath = `${testServerOrigin}/test/workbox-window/static/`; @@ -152,7 +152,14 @@ describe(`[workbox-window] Workbox`, function() { }); it(`reports all events for an external SW registration`, async function() { + // Skip this test in Safari due to this flakiness issue: + // https://github.com/GoogleChrome/workbox/issues/2150 + if (seleniumBrowser.getId() === 'safari') { + this.skip(); + } + const firstTab = await getLastWindowHandle(); + await webdriver.switchTo().window(firstTab); await executeAsyncAndCatch(async (cb) => { try { diff --git a/test/workbox-window/static/sw-window-ready.js b/test/workbox-window/static/sw-window-ready.js deleted file mode 100644 index d4af1c4b1..000000000 --- a/test/workbox-window/static/sw-window-ready.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. -*/ - -addEventListener('install', (event) => event.waitUntil(skipWaiting())); -addEventListener('activate', (event) => event.waitUntil(clients.claim())); - -addEventListener('message', async (event) => { - // Assert the type and meta are correct. - if (event.data.type === 'WINDOW_READY' && - event.data.meta === 'workbox-window') { - const windows = await clients.matchAll({ - type: 'window', - includeUncontrolled: true, - }); - for (const win of windows) { - win.postMessage({type: 'sw:message:ready'}); - } - } -}); diff --git a/test/workbox-window/window/test-Workbox.mjs b/test/workbox-window/window/test-Workbox.mjs index f9a29b50e..8684ef7d3 100644 --- a/test/workbox-window/window/test-Workbox.mjs +++ b/test/workbox-window/window/test-Workbox.mjs @@ -301,11 +301,19 @@ describe(`[workbox-window] Workbox`, function() { expect(reg.update.callCount).to.equal(1); }); - it(`triggers an updatefound event if the SW was udpated`, async function() { + it(`triggers an updatefound event if the SW was updated`, async function() { const scriptURL = navigator.serviceWorker.controller.scriptURL; const wb = new Workbox(scriptURL); + const reg = await wb.register(); + const updatefoundPromise = new Promise((resolve) => { + reg.addEventListener('updatefound', () => { + expect(reg.installing).to.not.equal(navigator.serviceWorker.controller); + resolve(); + }); + }); + await wb.controlling; // Update the SW after so an update check triggers an update. @@ -313,12 +321,7 @@ describe(`[workbox-window] Workbox`, function() { wb.update(); - await new Promise((resolve) => { - reg.addEventListener('updatefound', () => { - expect(reg.installing).to.not.equal(navigator.serviceWorker.controller); - resolve(); - }); - }); + await updatefoundPromise; }); describe(`logs in development-only`, function() {