From 615c719e808c27753959d73f8929761fba6dbc54 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Fri, 19 Feb 2021 01:52:42 -0800 Subject: [PATCH 1/7] Added comments to shouldInstallWorker * No code changes * Documenting the current state before I introduce a change in my next commit --- src/managers/ServiceWorkerManager.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/managers/ServiceWorkerManager.ts b/src/managers/ServiceWorkerManager.ts index 13f9c07c6..433ce3ae0 100644 --- a/src/managers/ServiceWorkerManager.ts +++ b/src/managers/ServiceWorkerManager.ts @@ -136,34 +136,38 @@ export class ServiceWorkerManager { } private async shouldInstallWorker(): Promise { + // 1. Does the browser support ServiceWorkers? if (!Environment.supportsServiceWorkers()) return false; + // 2. Is OneSignal initialized? if (!OneSignal.config) return false; + // 3. Will the service worker be installed on os.tc instead of the current domain? if (OneSignal.config.subdomain) { // No, if configured to use our subdomain (AKA HTTP setup) AND this is on their page (HTTP or HTTPS). // But since safari does not need subscription workaround, installing SW for session tracking. if ( - OneSignal.environmentInfo.browserType !== "safari" && + OneSignal.environmentInfo.browserType !== "safari" && SdkEnvironment.getWindowEnv() === WindowEnvironmentKind.Host ) { return false; } } + // 4. Is a OneSignal ServiceWorker not installed now?, if not and + // notification permissions are enabled we should install. + // This prevents an unnecessary install which saves bandwidth const workerState = await this.getActiveState(); - // If there isn't a SW or it isn't OneSignal's only install our SW if notification permissions are enabled - // This prevents an unnessary install which saves bandwidth if (workerState === ServiceWorkerActiveState.None || workerState === ServiceWorkerActiveState.ThirdParty) { const permission = await OneSignal.context.permissionManager.getNotificationPermission( OneSignal.config!.safariWebId ); - return permission === "granted"; } + // 5. We have a OneSignal ServiceWorker installed, is there an update? return this.workerNeedsUpdate(); } From 4997ab40d096bde30686101ac0f26020c8f025d9 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Fri, 19 Feb 2021 05:05:13 -0800 Subject: [PATCH 2/7] ServiceWorkerHelper.getServiceWorkerHref refactor * Moved logic into smaller parts that is used the generate the full href for the OneSignal ServiceWorker. - No result changes. * Simplied the else part where we check for ServiceWorkerActiveState - As we always want to install service worker A in all other cases. * The benifits of this refactor will be used in the next commit --- src/helpers/ServiceWorkerHelper.ts | 24 +++++++++++++++++------- src/managers/ServiceWorkerManager.ts | 17 +++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/helpers/ServiceWorkerHelper.ts b/src/helpers/ServiceWorkerHelper.ts index afe2dde7c..7bf731faf 100755 --- a/src/helpers/ServiceWorkerHelper.ts +++ b/src/helpers/ServiceWorkerHelper.ts @@ -16,20 +16,30 @@ import { OSServiceWorkerFields } from "../service-worker/types"; declare var self: ServiceWorkerGlobalScope & OSServiceWorkerFields; export default class ServiceWorkerHelper { - public static getServiceWorkerHref( + + // Get the href of the OneSiganl ServiceWorker that should be installed + // If a OneSignal ServiceWorker is already installed we will use an alternating name + // to force an update to the worker. + public static getAlternatingServiceWorkerHref( workerState: ServiceWorkerActiveState, - config: ServiceWorkerManagerConfig): string { - let workerFullPath = ""; + config: ServiceWorkerManagerConfig, + appId: string + ): string { + let workerFullPath: string; // Determine which worker to install if (workerState === ServiceWorkerActiveState.WorkerA) workerFullPath = config.workerBPath.getFullPath(); - else if (workerState === ServiceWorkerActiveState.WorkerB || - workerState === ServiceWorkerActiveState.ThirdParty || - workerState === ServiceWorkerActiveState.None) + else workerFullPath = config.workerAPath.getFullPath(); - return new URL(workerFullPath, OneSignalUtils.getBaseUrl()).href; + return ServiceWorkerHelper.appendServiceWorkerParams(workerFullPath, appId); + } + + private static appendServiceWorkerParams(workerFullPath: string, appId: string): string { + const fullPath = new URL(workerFullPath, OneSignalUtils.getBaseUrl()).href; + const appIdHasQueryParam = Utils.encodeHashAsUriComponent({appId}); + return `${fullPath}?${appIdHasQueryParam}`; } public static async upsertSession( diff --git a/src/managers/ServiceWorkerManager.ts b/src/managers/ServiceWorkerManager.ts index 433ce3ae0..32c2eb5f8 100644 --- a/src/managers/ServiceWorkerManager.ts +++ b/src/managers/ServiceWorkerManager.ts @@ -366,15 +366,16 @@ export class ServiceWorkerManager { Log.info(`[Service Worker Installation] 3rd party service worker detected.`); } - const workerFullPath = ServiceWorkerHelper.getServiceWorkerHref(workerState, this.config); - const installUrlQueryParams = Utils.encodeHashAsUriComponent({ - appId: this.context.appConfig.appId - }); - const fullWorkerPath = `${workerFullPath}?${installUrlQueryParams}`; + const workerHref = ServiceWorkerHelper.getAlternatingServiceWorkerHref( + workerState, + this.config, + this.context.appConfig.appId + ); + const scope = `${OneSignalUtils.getBaseUrl()}${this.config.registrationOptions.scope}`; - Log.info(`[Service Worker Installation] Installing service worker ${fullWorkerPath} ${scope}.`); + Log.info(`[Service Worker Installation] Installing service worker ${workerHref} ${scope}.`); try { - await navigator.serviceWorker.register(fullWorkerPath, { scope }); + await navigator.serviceWorker.register(workerHref, { scope }); } catch (error) { Log.error(`[Service Worker Installation] Installing service worker failed ${error}`); // Try accessing the service worker path directly to find out what the problem is and report it to OneSignal api. @@ -385,7 +386,7 @@ export class ServiceWorkerManager { if (env === WindowEnvironmentKind.OneSignalSubscriptionPopup) throw error; - const response = await fetch(fullWorkerPath); + const response = await fetch(workerHref); if (response.status === 403 || response.status === 404) throw new ServiceWorkerRegistrationError(response.status, response.statusText); From 4d42034e1ed891ff2d3949463d0ba7403a4d5d23 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Fri, 19 Feb 2021 05:21:52 -0800 Subject: [PATCH 3/7] Added SW param check to shouldInstallWorker * Added a new changedServiceWorkerParams method which check if; - OneSignal ServiceWorker path, filename, or query params changed. - OneSignal ServierWorker scope changed * This is a new check that was added to shouldInstallWorker * A developer might change the OneSignal service worker params to take advantage of the PR #745 "Remove ServiceWorker Page Control Requirement" - This way the new OneSignal SW scope can be setup while their new root scoped SW can be installed at the same time. --- src/helpers/ServiceWorkerHelper.ts | 9 +++++ src/managers/ServiceWorkerManager.ts | 57 ++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/helpers/ServiceWorkerHelper.ts b/src/helpers/ServiceWorkerHelper.ts index 7bf731faf..970d0a16f 100755 --- a/src/helpers/ServiceWorkerHelper.ts +++ b/src/helpers/ServiceWorkerHelper.ts @@ -12,6 +12,7 @@ import { OutcomesConfig } from "../models/Outcomes"; import OutcomesHelper from './shared/OutcomesHelper'; import { cancelableTimeout, CancelableTimeoutPromise } from './sw/CancelableTimeout'; import { OSServiceWorkerFields } from "../service-worker/types"; +import Utils from "../context/shared/utils/Utils"; declare var self: ServiceWorkerGlobalScope & OSServiceWorkerFields; @@ -36,6 +37,14 @@ export default class ServiceWorkerHelper { return ServiceWorkerHelper.appendServiceWorkerParams(workerFullPath, appId); } + public static getPossibleServiceWorkerHrefs( + config: ServiceWorkerManagerConfig, + appId: string + ): string[] { + const workerFullPaths = [config.workerAPath.getFullPath(), config.workerBPath.getFullPath()]; + return workerFullPaths.map((href) => ServiceWorkerHelper.appendServiceWorkerParams(href, appId)); + } + private static appendServiceWorkerParams(workerFullPath: string, appId: string): string { const fullPath = new URL(workerFullPath, OneSignalUtils.getBaseUrl()).href; const appIdHasQueryParam = Utils.encodeHashAsUriComponent({appId}); diff --git a/src/managers/ServiceWorkerManager.ts b/src/managers/ServiceWorkerManager.ts index 32c2eb5f8..a96fbcaf1 100644 --- a/src/managers/ServiceWorkerManager.ts +++ b/src/managers/ServiceWorkerManager.ts @@ -156,9 +156,9 @@ export class ServiceWorkerManager { } } - // 4. Is a OneSignal ServiceWorker not installed now?, if not and - // notification permissions are enabled we should install. - // This prevents an unnecessary install which saves bandwidth + // 4. Is a OneSignal ServiceWorker not installed now? + // If not and notification permissions are enabled we should install. + // This prevents an unnecessary install of the OneSignal worker which saves bandwidth const workerState = await this.getActiveState(); if (workerState === ServiceWorkerActiveState.None || workerState === ServiceWorkerActiveState.ThirdParty) { const permission = await OneSignal.context.permissionManager.getNotificationPermission( @@ -167,10 +167,59 @@ export class ServiceWorkerManager { return permission === "granted"; } - // 5. We have a OneSignal ServiceWorker installed, is there an update? + // 5. We have a OneSignal ServiceWorker installed, but did the path or scope of the ServiceWorker change? + if (await this.haveParamsChanged()) { + return true; + } + + // 6. We have a OneSignal ServiceWorker installed, is there an update? return this.workerNeedsUpdate(); } + private async haveParamsChanged(): Promise { + // 1. No workerRegistration + const workerRegistration = await this.context.serviceWorkerManager.getRegistration(); + if (!workerRegistration) { + Log.info( + "[changedServiceWorkerParams] workerRegistration not found at scope", + this.config.registrationOptions.scope + ); + return true; + } + + // 2. Different scope + const existingSwScope = new URL(workerRegistration.scope).pathname; + const configuredSwScope = this.config.registrationOptions.scope; + if (existingSwScope != configuredSwScope) { + Log.info( + "[changedServiceWorkerParams] ServiceWorker scope changing", + { a_old: existingSwScope, b_new: configuredSwScope } + ); + return true; + } + + // 3. Different href?, asking if (path + filename [A or B] + queryParams) is different + const availableWorker = ServiceWorkerUtilHelper.getAvailableServiceWorker(workerRegistration); + const serviceWorkerHrefs = ServiceWorkerHelper.getPossibleServiceWorkerHrefs( + this.config, + this.context.appConfig.appId + ); + // 3.1 If we can't get a scriptURL assume it is different + if (!availableWorker?.scriptURL) { + return true; + } + // 3.2 We don't care if the only differences is between OneSignal's A(Worker) vs B(WorkerUpdater) filename. + if (serviceWorkerHrefs.indexOf(availableWorker.scriptURL) === -1) { + Log.info( + "[changedServiceWorkerParams] ServiceWorker href changing:", + { a_old: availableWorker?.scriptURL, b_new: serviceWorkerHrefs } + ); + return true; + } + + return false; + } + /** * Performs a service worker update by swapping out the current service worker * with a content-identical but differently named alternate service worker From dd39b2a14e106c4ebc3138494dddf58d93f3c2a1 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Fri, 19 Feb 2021 05:22:49 -0800 Subject: [PATCH 4/7] Added more logging to the ServiceWorkerManager --- src/managers/ServiceWorkerManager.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/managers/ServiceWorkerManager.ts b/src/managers/ServiceWorkerManager.ts index a96fbcaf1..0be88c522 100644 --- a/src/managers/ServiceWorkerManager.ts +++ b/src/managers/ServiceWorkerManager.ts @@ -160,11 +160,16 @@ export class ServiceWorkerManager { // If not and notification permissions are enabled we should install. // This prevents an unnecessary install of the OneSignal worker which saves bandwidth const workerState = await this.getActiveState(); + Log.debug("[shouldInstallWorker] workerState", workerState); if (workerState === ServiceWorkerActiveState.None || workerState === ServiceWorkerActiveState.ThirdParty) { const permission = await OneSignal.context.permissionManager.getNotificationPermission( OneSignal.config!.safariWebId ); - return permission === "granted"; + const notificationsEnabled = permission === "granted"; + if (notificationsEnabled) { + Log.info("[shouldInstallWorker] Notification Permissions enabled, will install ServiceWorker"); + } + return notificationsEnabled; } // 5. We have a OneSignal ServiceWorker installed, but did the path or scope of the ServiceWorker change? @@ -304,6 +309,7 @@ export class ServiceWorkerManager { } public async establishServiceWorkerChannel() { + Log.debug('establishServiceWorkerChannel'); const workerMessenger = this.context.workerMessenger; workerMessenger.off(); From 50dd84ff97db3025aee147255f408ffbb354c3cc Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Tue, 23 Feb 2021 05:09:59 -0800 Subject: [PATCH 5/7] Multiple registartions MockServiceWorkerContainer * Multiple service workers are possible with browsers, if their scopes are different. Updated the mock to handle this. --- .../models/MockServiceWorkerContainer.ts | 41 ++++++++++++++----- .../models/MockServiceWorkerRegistration.ts | 2 +- test/unit/meta/mockServiceWorker.ts | 10 ++++- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts b/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts index 27d4631f5..252dd4266 100644 --- a/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts +++ b/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts @@ -22,10 +22,10 @@ export abstract class MockServiceWorkerContainer implements ServiceWorkerContain onmessageerror: ((this: ServiceWorkerContainer, ev: MessageEvent) => any) | null; private dispatchEventUtil: DispatchEventUtil = new DispatchEventUtil(); - public serviceWorkerRegistration: ServiceWorkerRegistration | null; + private serviceWorkerRegistrations: Map; constructor() { - this.serviceWorkerRegistration = null; + this.serviceWorkerRegistrations = new Map(); this._controller = null; this.onmessage = null; this.onmessageerror = null; @@ -40,17 +40,15 @@ export abstract class MockServiceWorkerContainer implements ServiceWorkerContain return this.dispatchEventUtil.dispatchEvent(evt); } - async getRegistration(_clientURL?: string): Promise { - return this.serviceWorkerRegistration || undefined; + async getRegistration(clientURL?: string): Promise { + return this.serviceWorkerRegistrations.get(clientURL || "/"); } async getRegistrations(): Promise { - if (this.serviceWorkerRegistration) - return [this.serviceWorkerRegistration]; - return []; + return Array.from(this.serviceWorkerRegistrations.values()); } - async register(scriptURL: string, _options?: RegistrationOptions): Promise { + async register(scriptURL: string, options?: RegistrationOptions): Promise { if (scriptURL.startsWith('/')) { const fakeScriptUrl = new URL(window.location.toString()); scriptURL = fakeScriptUrl.origin + scriptURL; @@ -59,14 +57,31 @@ export abstract class MockServiceWorkerContainer implements ServiceWorkerContain const mockSw = new MockServiceWorker(); mockSw.scriptURL = scriptURL; mockSw.state = 'activated'; - + this._controller = mockSw; const swReg = new MockServiceWorkerRegistration(); swReg.active = this._controller; - this.serviceWorkerRegistration = swReg; - return this.serviceWorkerRegistration; + const scope = MockServiceWorkerContainer.getScopeAsPathname(options); + this.serviceWorkerRegistrations.set(scope, swReg); + + return swReg; + } + + // RegistrationOptions.scope could be falsely or a string in a URL or pathname format. + // This will always give us a pathname, defaulting to "/" if falsely. + private static getScopeAsPathname(options?: RegistrationOptions): string { + if (!options?.scope) { + return "/"; + } + + try { + return new URL(options.scope).pathname; + } catch(_e) { + // Not a valid URL, assuming it's a path + return options.scope; + } } removeEventListener(type: K, listener: (this: ServiceWorkerContainer, ev: ServiceWorkerContainerEventMap[K]) => any, options?: boolean | EventListenerOptions): void; @@ -79,4 +94,8 @@ export abstract class MockServiceWorkerContainer implements ServiceWorkerContain startMessages(): void { } + mockUnregister(scope: string) { + this.serviceWorkerRegistrations.delete(scope); + } + } diff --git a/test/support/mocks/service-workers/models/MockServiceWorkerRegistration.ts b/test/support/mocks/service-workers/models/MockServiceWorkerRegistration.ts index 087177d93..29c873947 100644 --- a/test/support/mocks/service-workers/models/MockServiceWorkerRegistration.ts +++ b/test/support/mocks/service-workers/models/MockServiceWorkerRegistration.ts @@ -48,7 +48,7 @@ export class MockServiceWorkerRegistration implements ServiceWorkerRegistration async unregister(): Promise { const container = navigator.serviceWorker as MockServiceWorkerContainer; - container.serviceWorkerRegistration = null; + container.mockUnregister(new URL(this.scope).pathname); this.active = null; return true; } diff --git a/test/unit/meta/mockServiceWorker.ts b/test/unit/meta/mockServiceWorker.ts index 88665d9fb..074e41d3a 100644 --- a/test/unit/meta/mockServiceWorker.ts +++ b/test/unit/meta/mockServiceWorker.ts @@ -48,12 +48,20 @@ test('mock service worker registration should return the registered worker', asy t.deepEqual(registrations, [registration]); }); +test('mock service worker getRegistrations should return multiple registered workers', async t => { + const expectedRegistrations = [] as ServiceWorkerRegistration[]; + expectedRegistrations.push(await navigator.serviceWorker.register('/workerA.js', { scope: '/' })); + expectedRegistrations.push(await navigator.serviceWorker.register('/workerB.js', { scope: '/mypath/' })); + + const registrations = await navigator.serviceWorker.getRegistrations(); + t.deepEqual(registrations, expectedRegistrations); +}); test('mock service worker unregistration should return no registered workers', async t => { await navigator.serviceWorker.register('/worker.js', { scope: '/' }); const initialRegistration = await navigator.serviceWorker.getRegistration(); - await initialRegistration.unregister(); + await initialRegistration!.unregister(); const postUnsubscribeRegistration = await navigator.serviceWorker.getRegistration(); t.is(postUnsubscribeRegistration, undefined); From 3bc851ceefd9d908483828702250d6a854f99449 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Tue, 23 Feb 2021 05:38:00 -0800 Subject: [PATCH 6/7] Scope fallback to MockServiceWorkerContainer * MockServiceWorkerContainer.getRegistration - Added match any SW's that are at a higher scope than the one we are querying for as they are under it's control. - This was observed behavoir when testing on real browsers. --- .../models/MockServiceWorkerContainer.ts | 18 +++++++++++++++++- test/unit/meta/mockServiceWorker.ts | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts b/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts index 252dd4266..579807784 100644 --- a/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts +++ b/test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts @@ -41,7 +41,23 @@ export abstract class MockServiceWorkerContainer implements ServiceWorkerContain } async getRegistration(clientURL?: string): Promise { - return this.serviceWorkerRegistrations.get(clientURL || "/"); + const scope = clientURL || "/"; + + // 1. If we find an exact path match + let registration = this.serviceWorkerRegistrations.get(scope); + if (registration) { + return registration; + } + + // 2. Match any SW's that are at a higher scope than the one we are querying for as they are under it's control. + // WARNING: This mock implementation does not consider which one is correct if more than one applies the scope. + this.serviceWorkerRegistrations.forEach((value, key) => { + if (scope.startsWith(key)) { + registration = value; + } + }); + + return registration; } async getRegistrations(): Promise { diff --git a/test/unit/meta/mockServiceWorker.ts b/test/unit/meta/mockServiceWorker.ts index 074e41d3a..c0e469cab 100644 --- a/test/unit/meta/mockServiceWorker.ts +++ b/test/unit/meta/mockServiceWorker.ts @@ -57,6 +57,19 @@ test('mock service worker getRegistrations should return multiple registered wor t.deepEqual(registrations, expectedRegistrations); }); +test('mock service worker getRegistration should return higher path worker', async t => { + const expected = await navigator.serviceWorker.register('/workerA.js', { scope: '/' }); + const actual = await navigator.serviceWorker.getRegistration("/some/scope/"); + t.deepEqual(actual, expected); +}); + +test('mock service worker getRegistration should return specific path if a higher path worker exists too', async t => { + const expected = await navigator.serviceWorker.register('/workerB.js', { scope: '/mypath/' }); + await navigator.serviceWorker.register('/workerA.js', { scope: '/' }); + const actual = await navigator.serviceWorker.getRegistration("/mypath/"); + t.deepEqual(actual, expected); +}); + test('mock service worker unregistration should return no registered workers', async t => { await navigator.serviceWorker.register('/worker.js', { scope: '/' }); From e1b89b1dcc99527716358bd61226e2ffd7db39b5 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Tue, 23 Feb 2021 05:55:59 -0800 Subject: [PATCH 7/7] Added scope changed test --- test/unit/managers/ServiceWorkerManager.ts | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/unit/managers/ServiceWorkerManager.ts b/test/unit/managers/ServiceWorkerManager.ts index 29fe2cd17..6481ba00e 100644 --- a/test/unit/managers/ServiceWorkerManager.ts +++ b/test/unit/managers/ServiceWorkerManager.ts @@ -264,6 +264,45 @@ test('installWorker() installs Worker B and then A when Worker A is out of date' t.is(spy.callCount, 4); }); +test('installWorker() installs Worker new scope when it changes', async t => { + await TestEnvironment.initialize({ + httpOrHttps: HttpHttpsEnvironment.Https + }); + sandbox.stub(Notification, "permission").value("granted"); + // We don't want the version number check from "workerNeedsUpdate" interfering with this test. + sandbox.stub(ServiceWorkerManager.prototype, "workerNeedsUpdate").resolves(false); + + const serviceWorkerConfig = { + workerAPath: new Path('/Worker-A.js'), + workerBPath: new Path('/Worker-B.js'), + registrationOptions: { scope: '/' } + }; + const manager = new ServiceWorkerManager(OneSignal.context, serviceWorkerConfig); + + // 1. Install ServiceWorker A and assert it was ServiceWorker A + await manager.installWorker(); + + // 2. Attempt to install again, but with a different scope + serviceWorkerConfig.registrationOptions.scope = '/push/onesignal/'; + const spyRegister = sandbox.spy(navigator.serviceWorker, 'register'); + await manager.installWorker(); + + // 3. Assert we did register our worker under the new scope. + const appId = OneSignal.context.appConfig.appId; + t.deepEqual(spyRegister.getCalls().map(call => call.args), [ + [ + `https://localhost:3001/Worker-B.js?appId=${appId}`, + { scope: 'https://localhost:3001/push/onesignal/' } + ] + ]); + + // 4. Ensure we kept the original ServiceWorker. + // A. Original could contain more than just OneSignal code + // B. New ServiceWorker instance will have it's own pushToken, this may have not been sent onesignal.com yet. + const orgRegistration = await navigator.serviceWorker.getRegistration("/"); + t.is(new URL(orgRegistration!.scope).pathname, "/"); +}); + test('Server worker register URL correct when service worker path is a absolute URL', async t => { await TestEnvironment.initialize({ httpOrHttps: HttpHttpsEnvironment.Https