From ef0435f1edc1a6fa4ce44945d6d9131adac37367 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 25 Apr 2019 16:51:07 +0300 Subject: [PATCH 1/8] refactor(service-worker): rename `RegistrationOptions` to `SwRegistrationOptions` This is in preparation of making `RegistrationOptions` part of the public API (in a subsequent commit). --- packages/service-worker/src/module.ts | 12 ++++++------ packages/service-worker/test/comm_spec.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/service-worker/src/module.ts b/packages/service-worker/src/module.ts index 4cc7bc3f696fd..3d324bc37d1be 100644 --- a/packages/service-worker/src/module.ts +++ b/packages/service-worker/src/module.ts @@ -14,7 +14,7 @@ import {NgswCommChannel} from './low_level'; import {SwPush} from './push'; import {SwUpdate} from './update'; -export abstract class RegistrationOptions { +export abstract class SwRegistrationOptions { scope?: string; enabled?: boolean; } @@ -22,7 +22,7 @@ export abstract class RegistrationOptions { export const SCRIPT = new InjectionToken('NGSW_REGISTER_SCRIPT'); export function ngswAppInitializer( - injector: Injector, script: string, options: RegistrationOptions, + injector: Injector, script: string, options: SwRegistrationOptions, platformId: string): Function { const initializer = () => { const app = injector.get(ApplicationRef); @@ -50,7 +50,7 @@ export function ngswAppInitializer( } export function ngswCommChannelFactory( - opts: RegistrationOptions, platformId: string): NgswCommChannel { + opts: SwRegistrationOptions, platformId: string): NgswCommChannel { return new NgswCommChannel( isPlatformBrowser(platformId) && opts.enabled !== false ? navigator.serviceWorker : undefined); @@ -75,16 +75,16 @@ export class ServiceWorkerModule { ngModule: ServiceWorkerModule, providers: [ {provide: SCRIPT, useValue: script}, - {provide: RegistrationOptions, useValue: opts}, + {provide: SwRegistrationOptions, useValue: opts}, { provide: NgswCommChannel, useFactory: ngswCommChannelFactory, - deps: [RegistrationOptions, PLATFORM_ID] + deps: [SwRegistrationOptions, PLATFORM_ID] }, { provide: APP_INITIALIZER, useFactory: ngswAppInitializer, - deps: [Injector, SCRIPT, RegistrationOptions, PLATFORM_ID], + deps: [Injector, SCRIPT, SwRegistrationOptions, PLATFORM_ID], multi: true, }, ], diff --git a/packages/service-worker/test/comm_spec.ts b/packages/service-worker/test/comm_spec.ts index 714e9ba79dd8c..09b9af248ed81 100644 --- a/packages/service-worker/test/comm_spec.ts +++ b/packages/service-worker/test/comm_spec.ts @@ -9,7 +9,7 @@ import {PLATFORM_ID} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {NgswCommChannel} from '@angular/service-worker/src/low_level'; -import {RegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; +import {SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; import {SwPush} from '@angular/service-worker/src/push'; import {SwUpdate} from '@angular/service-worker/src/update'; import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock'; @@ -52,10 +52,10 @@ import {async_fit, async_it} from './async'; TestBed.configureTestingModule({ providers: [ {provide: PLATFORM_ID, useValue: 'server'}, - {provide: RegistrationOptions, useValue: {enabled: true}}, { + {provide: SwRegistrationOptions, useValue: {enabled: true}}, { provide: NgswCommChannel, useFactory: ngswCommChannelFactory, - deps: [RegistrationOptions, PLATFORM_ID] + deps: [SwRegistrationOptions, PLATFORM_ID] } ] }); @@ -66,10 +66,10 @@ import {async_fit, async_it} from './async'; TestBed.configureTestingModule({ providers: [ {provide: PLATFORM_ID, useValue: 'browser'}, - {provide: RegistrationOptions, useValue: {enabled: false}}, { + {provide: SwRegistrationOptions, useValue: {enabled: false}}, { provide: NgswCommChannel, useFactory: ngswCommChannelFactory, - deps: [RegistrationOptions, PLATFORM_ID] + deps: [SwRegistrationOptions, PLATFORM_ID] } ] }); @@ -80,11 +80,11 @@ import {async_fit, async_it} from './async'; TestBed.configureTestingModule({ providers: [ {provide: PLATFORM_ID, useValue: 'browser'}, - {provide: RegistrationOptions, useValue: {enabled: true}}, + {provide: SwRegistrationOptions, useValue: {enabled: true}}, { provide: NgswCommChannel, useFactory: ngswCommChannelFactory, - deps: [RegistrationOptions, PLATFORM_ID], + deps: [SwRegistrationOptions, PLATFORM_ID], }, ], }); @@ -110,10 +110,10 @@ import {async_fit, async_it} from './async'; TestBed.configureTestingModule({ providers: [ {provide: PLATFORM_ID, useValue: 'browser'}, - {provide: RegistrationOptions, useValue: {enabled: true}}, { + {provide: SwRegistrationOptions, useValue: {enabled: true}}, { provide: NgswCommChannel, useFactory: ngswCommChannelFactory, - deps: [RegistrationOptions, PLATFORM_ID] + deps: [SwRegistrationOptions, PLATFORM_ID] } ] }); From 5874101516acfade6aee0f6b45c41b46feeed8d3 Mon Sep 17 00:00:00 2001 From: deebloo Date: Thu, 25 Apr 2019 16:51:07 +0300 Subject: [PATCH 2/8] feat(service-worker): expose `SwRegistrationOptions` token to allow runtime config Previously, the ServiceWorker registration options should be defined as an object literal (in order for them to be compatible with Ahead-of-Time compilation), thus making it impossible to base the ServiceWorker behavior on runtime conditions. This commit allows specifying the registration options using a regular provider, which means that it can take advantage of the `useFactory` option to determine the config at runtime, while still remaining compatible with AoT compilation. --- packages/service-worker/src/index.ts | 2 +- packages/service-worker/src/module.ts | 8 +++- packages/service-worker/test/comm_spec.ts | 41 ++++++++++++++++++- .../service-worker/service-worker.d.ts | 10 +++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/service-worker/src/index.ts b/packages/service-worker/src/index.ts index a7defed04f7a3..57bef7b8bce76 100644 --- a/packages/service-worker/src/index.ts +++ b/packages/service-worker/src/index.ts @@ -15,6 +15,6 @@ */ export {UpdateActivatedEvent, UpdateAvailableEvent} from './low_level'; -export {ServiceWorkerModule} from './module'; +export {ServiceWorkerModule, SwRegistrationOptions} from './module'; export {SwPush} from './push'; export {SwUpdate} from './update'; diff --git a/packages/service-worker/src/module.ts b/packages/service-worker/src/module.ts index 3d324bc37d1be..15f174688b3b7 100644 --- a/packages/service-worker/src/module.ts +++ b/packages/service-worker/src/module.ts @@ -14,6 +14,12 @@ import {NgswCommChannel} from './low_level'; import {SwPush} from './push'; import {SwUpdate} from './update'; +/** + * Token that can be used to provide options for `ServiceWorkerModule` outside of + * `ServiceWorkerModule.register()`. + * + * @publicApi + */ export abstract class SwRegistrationOptions { scope?: string; enabled?: boolean; @@ -69,7 +75,7 @@ export class ServiceWorkerModule { * If `enabled` is set to `false` in the given options, the module will behave as if service * workers are not supported by the browser, and the service worker will not be registered. */ - static register(script: string, opts: {scope?: string; enabled?: boolean;} = {}): + static register(script: string, opts: SwRegistrationOptions = {}): ModuleWithProviders { return { ngModule: ServiceWorkerModule, diff --git a/packages/service-worker/test/comm_spec.ts b/packages/service-worker/test/comm_spec.ts index 09b9af248ed81..3e313bc445eff 100644 --- a/packages/service-worker/test/comm_spec.ts +++ b/packages/service-worker/test/comm_spec.ts @@ -9,7 +9,7 @@ import {PLATFORM_ID} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {NgswCommChannel} from '@angular/service-worker/src/low_level'; -import {SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; +import {ServiceWorkerModule, SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; import {SwPush} from '@angular/service-worker/src/push'; import {SwUpdate} from '@angular/service-worker/src/update'; import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock'; @@ -47,6 +47,45 @@ import {async_fit, async_it} from './async'; }); }); + describe('ServiceWorkerModule config', () => { + it('SwUpdate isEnabled is false when configuring via static method', () => { + TestBed.configureTestingModule( + {imports: [ServiceWorkerModule.register('', {enabled: false})]}); + + expect(TestBed.get(SwUpdate).isEnabled).toEqual(false); + }); + + it('SwUpdate isEnabled is true when configuring via static method', () => { + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('', {enabled: true})], + providers: [{provide: NgswCommChannel, useValue: comm}] + }); + + expect(TestBed.get(SwUpdate).isEnabled).toEqual(true); + }); + + it('SwUpdate isEnabled is false when configuring directly via token', () => { + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('')], + providers: [{provide: SwRegistrationOptions, useFactory: () => ({enabled: false})}] + }); + + expect(TestBed.get(SwUpdate).isEnabled).toEqual(false); + }); + + it('SwUpdate isEnabled is true when configuring directly via token', () => { + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('')], + providers: [ + {provide: NgswCommChannel, useValue: comm}, + {provide: SwRegistrationOptions, useFactory: () => ({enabled: true})} + ] + }); + + expect(TestBed.get(SwUpdate).isEnabled).toEqual(true); + }); + }); + describe('ngswCommChannelFactory', () => { it('gives disabled NgswCommChannel for platform-server', () => { TestBed.configureTestingModule({ diff --git a/tools/public_api_guard/service-worker/service-worker.d.ts b/tools/public_api_guard/service-worker/service-worker.d.ts index b24a965be2189..80eddf9878f64 100644 --- a/tools/public_api_guard/service-worker/service-worker.d.ts +++ b/tools/public_api_guard/service-worker/service-worker.d.ts @@ -1,8 +1,5 @@ export declare class ServiceWorkerModule { - static register(script: string, opts?: { - scope?: string; - enabled?: boolean; - }): ModuleWithProviders; + static register(script: string, opts?: SwRegistrationOptions): ModuleWithProviders; } export declare class SwPush { @@ -22,6 +19,11 @@ export declare class SwPush { unsubscribe(): Promise; } +export declare abstract class SwRegistrationOptions { + enabled?: boolean; + scope?: string; +} + export declare class SwUpdate { readonly activated: Observable; readonly available: Observable; From f6d1c63e780838e22b0ea34b5794f08e8de49020 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 25 Apr 2019 16:51:08 +0300 Subject: [PATCH 3/8] test(service-worker): expand `SwRegistrationOptions` tests and move to separate file --- packages/service-worker/test/comm_spec.ts | 41 +------- packages/service-worker/test/module_spec.ts | 109 ++++++++++++++++++++ 2 files changed, 110 insertions(+), 40 deletions(-) create mode 100644 packages/service-worker/test/module_spec.ts diff --git a/packages/service-worker/test/comm_spec.ts b/packages/service-worker/test/comm_spec.ts index 3e313bc445eff..09b9af248ed81 100644 --- a/packages/service-worker/test/comm_spec.ts +++ b/packages/service-worker/test/comm_spec.ts @@ -9,7 +9,7 @@ import {PLATFORM_ID} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {NgswCommChannel} from '@angular/service-worker/src/low_level'; -import {ServiceWorkerModule, SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; +import {SwRegistrationOptions, ngswCommChannelFactory} from '@angular/service-worker/src/module'; import {SwPush} from '@angular/service-worker/src/push'; import {SwUpdate} from '@angular/service-worker/src/update'; import {MockPushManager, MockPushSubscription, MockServiceWorkerContainer, MockServiceWorkerRegistration, patchDecodeBase64} from '@angular/service-worker/testing/mock'; @@ -47,45 +47,6 @@ import {async_fit, async_it} from './async'; }); }); - describe('ServiceWorkerModule config', () => { - it('SwUpdate isEnabled is false when configuring via static method', () => { - TestBed.configureTestingModule( - {imports: [ServiceWorkerModule.register('', {enabled: false})]}); - - expect(TestBed.get(SwUpdate).isEnabled).toEqual(false); - }); - - it('SwUpdate isEnabled is true when configuring via static method', () => { - TestBed.configureTestingModule({ - imports: [ServiceWorkerModule.register('', {enabled: true})], - providers: [{provide: NgswCommChannel, useValue: comm}] - }); - - expect(TestBed.get(SwUpdate).isEnabled).toEqual(true); - }); - - it('SwUpdate isEnabled is false when configuring directly via token', () => { - TestBed.configureTestingModule({ - imports: [ServiceWorkerModule.register('')], - providers: [{provide: SwRegistrationOptions, useFactory: () => ({enabled: false})}] - }); - - expect(TestBed.get(SwUpdate).isEnabled).toEqual(false); - }); - - it('SwUpdate isEnabled is true when configuring directly via token', () => { - TestBed.configureTestingModule({ - imports: [ServiceWorkerModule.register('')], - providers: [ - {provide: NgswCommChannel, useValue: comm}, - {provide: SwRegistrationOptions, useFactory: () => ({enabled: true})} - ] - }); - - expect(TestBed.get(SwUpdate).isEnabled).toEqual(true); - }); - }); - describe('ngswCommChannelFactory', () => { it('gives disabled NgswCommChannel for platform-server', () => { TestBed.configureTestingModule({ diff --git a/packages/service-worker/test/module_spec.ts b/packages/service-worker/test/module_spec.ts new file mode 100644 index 0000000000000..14e1e86b099c2 --- /dev/null +++ b/packages/service-worker/test/module_spec.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ApplicationRef, PLATFORM_ID} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {filter, take} from 'rxjs/operators'; + +import {ServiceWorkerModule, SwRegistrationOptions} from '../src/module'; +import {SwUpdate} from '../src/update'; + + +describe('ServiceWorkerModule', () => { + // Skip environments that don't support the minimum APIs needed to run these SW tests. + if ((typeof navigator === 'undefined') || (typeof navigator.serviceWorker === 'undefined')) { + return; + } + + let swRegisterSpy: jasmine.Spy; + + beforeEach(() => swRegisterSpy = spyOn(navigator.serviceWorker, 'register')); + + describe('register()', () => { + const configTestBed = async(opts: SwRegistrationOptions) => { + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('sw.js', opts)], + providers: [{provide: PLATFORM_ID, useValue: 'browser'}], + }); + + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + await appRef.isStable.pipe(filter(Boolean), take(1)).toPromise(); + }; + + it('sets the registration options', async() => { + await configTestBed({enabled: true, scope: 'foo'}); + + expect(TestBed.get(SwRegistrationOptions)).toEqual({enabled: true, scope: 'foo'}); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'foo'}); + }); + + it('can disable the SW', async() => { + await configTestBed({enabled: false}); + + expect(TestBed.get(SwUpdate).isEnabled).toBe(false); + expect(swRegisterSpy).not.toHaveBeenCalled(); + }); + + it('can enable the SW', async() => { + await configTestBed({enabled: true}); + + expect(TestBed.get(SwUpdate).isEnabled).toBe(true); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + }); + + it('defaults to enabling the SW', async() => { + await configTestBed({}); + expect(TestBed.get(SwUpdate).isEnabled).toBe(true); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + }); + }); + + describe('SwRegistrationOptions', () => { + const configTestBed = + async(providerOpts: SwRegistrationOptions, staticOpts?: SwRegistrationOptions) => { + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('sw.js', staticOpts || {scope: 'static'})], + providers: [ + {provide: PLATFORM_ID, useValue: 'browser'}, + {provide: SwRegistrationOptions, useFactory: () => providerOpts}, + ], + }); + + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + await appRef.isStable.pipe(filter(Boolean), take(1)).toPromise(); + }; + + it('sets the registration options (and overwrites those set via `.register()`', async() => { + await configTestBed({enabled: true, scope: 'provider'}); + + expect(TestBed.get(SwRegistrationOptions)).toEqual({enabled: true, scope: 'provider'}); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'}); + }); + + it('can disable the SW', async() => { + await configTestBed({enabled: false}, {enabled: true}); + + expect(TestBed.get(SwUpdate).isEnabled).toBe(false); + expect(swRegisterSpy).not.toHaveBeenCalled(); + }); + + it('can enable the SW', async() => { + await configTestBed({enabled: true}, {enabled: false}); + + expect(TestBed.get(SwUpdate).isEnabled).toBe(true); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + }); + + it('defaults to enabling the SW', async() => { + await configTestBed({}, {enabled: false}); + + expect(TestBed.get(SwUpdate).isEnabled).toBe(true); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + }); + }); +}); From ea877d526ef6ba201e214160913cf6aa643dbf32 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 25 Apr 2019 16:51:08 +0300 Subject: [PATCH 4/8] docs(service-worker): improve `SwRegistrationOptions` docs and add example --- .../registration-options/BUILD.bazel | 62 +++++++++++++++++++ .../e2e_test/registration-options_spec.ts | 27 ++++++++ .../registration-options/main.ts | 12 ++++ .../registration-options/module.ts | 49 +++++++++++++++ .../registration-options/ngsw-worker.js | 14 +++++ .../registration-options/start-server.js | 17 +++++ packages/service-worker/src/module.ts | 20 +++++- packages/tsconfig.json | 2 +- 8 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 packages/examples/service-worker/registration-options/BUILD.bazel create mode 100644 packages/examples/service-worker/registration-options/e2e_test/registration-options_spec.ts create mode 100644 packages/examples/service-worker/registration-options/main.ts create mode 100644 packages/examples/service-worker/registration-options/module.ts create mode 100644 packages/examples/service-worker/registration-options/ngsw-worker.js create mode 100644 packages/examples/service-worker/registration-options/start-server.js diff --git a/packages/examples/service-worker/registration-options/BUILD.bazel b/packages/examples/service-worker/registration-options/BUILD.bazel new file mode 100644 index 0000000000000..b9b9e7f5b6b59 --- /dev/null +++ b/packages/examples/service-worker/registration-options/BUILD.bazel @@ -0,0 +1,62 @@ +package(default_visibility = ["//visibility:public"]) + +load("//packages/bazel:index.bzl", "protractor_web_test_suite") +load("//tools:defaults.bzl", "ng_module", "ts_library") +load("@npm_bazel_typescript//:index.bzl", "ts_devserver") + +ng_module( + name = "sw_registration_options_examples", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*_spec.ts"], + ), + # TODO: FW-1004 Type checking is currently not complete. + type_check = False, + deps = [ + "//packages/core", + "//packages/platform-browser", + "//packages/platform-browser-dynamic", + "//packages/service-worker", + ], +) + +ts_library( + name = "sw_registration_options_e2e_tests_lib", + testonly = True, + srcs = glob(["**/e2e_test/*_spec.ts"]), + tsconfig = "//packages/examples:tsconfig-e2e.json", + deps = [ + "//packages/examples/test-utils", + "//packages/private/testing", + "@npm//@types/jasminewd2", + "@npm//protractor", + ], +) + +ts_devserver( + name = "devserver", + entry_module = "@angular/examples/service-worker/registration-options/main", + index_html = "//packages/examples:index.html", + port = 4200, + scripts = [ + "//tools/rxjs:rxjs_umd_modules", + "@npm//node_modules/tslib:tslib.js", + ], + static_files = [ + "ngsw-worker.js", + "@npm//node_modules/zone.js:dist/zone.js", + ], + deps = [":sw_registration_options_examples"], +) + +protractor_web_test_suite( + name = "protractor_tests", + data = ["//packages/bazel/src/protractor/utils"], + on_prepare = "start-server.js", + server = ":devserver", + deps = [ + ":sw_registration_options_e2e_tests_lib", + "@npm//protractor", + "@npm//selenium-webdriver", + ], +) diff --git a/packages/examples/service-worker/registration-options/e2e_test/registration-options_spec.ts b/packages/examples/service-worker/registration-options/e2e_test/registration-options_spec.ts new file mode 100644 index 0000000000000..5af093b1f41cd --- /dev/null +++ b/packages/examples/service-worker/registration-options/e2e_test/registration-options_spec.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {browser, by, element} from 'protractor'; +import {verifyNoBrowserErrors} from '../../../test-utils'; + +describe('SW `SwRegistrationOptions` example', () => { + const pageUrl = '/registration-options'; + const appElem = element(by.css('example-app')); + + afterEach(verifyNoBrowserErrors); + + it('not register the SW by default', () => { + browser.get(pageUrl); + expect(appElem.getText()).toBe('SW enabled: false'); + }); + + it('register the SW when navigating to `?sw=true`', () => { + browser.get(`${pageUrl}?sw=true`); + expect(appElem.getText()).toBe('SW enabled: true'); + }); +}); diff --git a/packages/examples/service-worker/registration-options/main.ts b/packages/examples/service-worker/registration-options/main.ts new file mode 100644 index 0000000000000..5ac1a58e47e74 --- /dev/null +++ b/packages/examples/service-worker/registration-options/main.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import {AppModuleNgFactory} from './module.ngfactory'; + +platformBrowserDynamic().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/packages/examples/service-worker/registration-options/module.ts b/packages/examples/service-worker/registration-options/module.ts new file mode 100644 index 0000000000000..c7128ccc02769 --- /dev/null +++ b/packages/examples/service-worker/registration-options/module.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// tslint:disable: no-duplicate-imports +import {Component} from '@angular/core'; +// #docregion registration-options +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {ServiceWorkerModule, SwRegistrationOptions} from '@angular/service-worker'; +// #enddocregion registration-options +import {SwUpdate} from '@angular/service-worker'; +// tslint:enable: no-duplicate-imports + +@Component({ + selector: 'example-app', + template: 'SW enabled: {{ swu.isEnabled }}', +}) +export class AppComponent { + constructor(readonly swu: SwUpdate) {} +} +// #docregion registration-options + +@NgModule({ + // #enddocregion registration-options + bootstrap: [ + AppComponent, + ], + declarations: [ + AppComponent, + ], + // #docregion registration-options + imports: [ + BrowserModule, + ServiceWorkerModule.register('ngsw-worker.js'), + ], + providers: [ + { + provide: SwRegistrationOptions, + useFactory: () => ({enabled: location.search.includes('sw=true')}), + }, + ], +}) +export class AppModule { +} +// #enddocregion registration-options diff --git a/packages/examples/service-worker/registration-options/ngsw-worker.js b/packages/examples/service-worker/registration-options/ngsw-worker.js new file mode 100644 index 0000000000000..45b1769cca5cd --- /dev/null +++ b/packages/examples/service-worker/registration-options/ngsw-worker.js @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Mock `ngsw-worker.js` used for testing the examples. +// Immediately takes over and unregisters itself. +self.addEventListener('install', evt => evt.waitUntil(self.skipWaiting())); +self.addEventListener( + 'activate', + evt => evt.waitUntil(self.clients.claim().then(() => self.registration.unregister()))); diff --git a/packages/examples/service-worker/registration-options/start-server.js b/packages/examples/service-worker/registration-options/start-server.js new file mode 100644 index 0000000000000..e7d432281a843 --- /dev/null +++ b/packages/examples/service-worker/registration-options/start-server.js @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const protractorUtils = require('@angular/bazel/protractor-utils'); +const protractor = require('protractor'); + +module.exports = async function(config) { + const {port} = await protractorUtils.runServer(config.workspace, config.server, '-port', []); + const serverUrl = `http://localhost:${port}`; + + protractor.browser.baseUrl = serverUrl; +}; diff --git a/packages/service-worker/src/module.ts b/packages/service-worker/src/module.ts index 15f174688b3b7..ddcd9102a43c8 100644 --- a/packages/service-worker/src/module.ts +++ b/packages/service-worker/src/module.ts @@ -18,11 +18,29 @@ import {SwUpdate} from './update'; * Token that can be used to provide options for `ServiceWorkerModule` outside of * `ServiceWorkerModule.register()`. * + * You can use this token to define a provider that generates the registration options at runtime, + * for example via a function call: + * + * {@example service-worker/registration-options/module.ts region="registration-options" + * header="app.module.ts" linenums="false"} + * * @publicApi */ export abstract class SwRegistrationOptions { - scope?: string; + /** + * Whether the ServiceWorker will be registered and the related services (such as `SwPush` and + * `SwUpdate`) will attempt to communicate and interact with it. + * + * Default: true + */ enabled?: boolean; + + /** + * A URL that defines the ServiceWorker's registration scope; that is, what range of URLs it can + * control. It will be used when calling + * [ServiceWorkerContainer#register()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register). + */ + scope?: string; } export const SCRIPT = new InjectionToken('NGSW_REGISTER_SCRIPT'); diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 8a3c22122fb34..f951fee5d1a5f 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -39,7 +39,7 @@ "examples/**/e2e_test/*", // Exclude the "main.ts" files for each example group because this file is used by // Bazel to launch the devserver and uses AOT compilation. - "examples/*/main.ts", + "examples/**/main.ts", "platform-server/integrationtest", "router/test/aot_ngsummary_test", ] From acec547e883788fcda143b6fe0a7b0e4d16cb70b Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Mon, 29 Jan 2018 01:38:16 +0900 Subject: [PATCH 5/8] feat(service-worker): allow configuring when the SW is registered Fixes #20970 --- aio/content/guide/service-worker-config.md | 12 +++++++ .../guide/service-worker-getting-started.md | 30 +++++++++--------- packages/service-worker/src/module.ts | 31 ++++++++++++++++++- .../service-worker/service-worker.d.ts | 2 ++ 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/aio/content/guide/service-worker-config.md b/aio/content/guide/service-worker-config.md index c2273b28cac0d..1cb77cfb561fc 100644 --- a/aio/content/guide/service-worker-config.md +++ b/aio/content/guide/service-worker-config.md @@ -218,3 +218,15 @@ If the field is omitted, it defaults to: '!/**/*__*/**', // Exclude URLs containing `__` in any other segment. ] ``` + +## `register options` + + + You can pass some options to the `register()` method. +- enabled: optional parameter, by default is true, if enabled is false, the module will behave like the browser not support service worker, and service worker will not be registered. +- scope: optional parameter, to specify the subset of your content that you want the service worker to control. +- registrationStrategy: optional parameter, specify a strategy that determines when to register the service worker, the available options are: + - registerWhenStable: this is the default behavior, the service worker will register when the application is stable (no microTasks or macroTasks remain). + - registerImmediately: register immediately without waiting the application to become stable. + - registerDelay:timeout : register after the timeout period, `timeout` is the number of milliseconds to delay registration. For example `registerDelay:5000` would register the service worker after 5 seconds. If the number of `timeout` is not given (`registerDelay`), by default, `timeout` will be `0`, but it is not equal to `registerImmediately`, it will still run a `setTimeout(register, 0)` to wait all `microTasks` to finish then perform registration of the service worker. + - A factory to get Observable : you can also specify a factory which returns an Observable, the service worker will be registered the first time that a value is emitted by the Observable. \ No newline at end of file diff --git a/aio/content/guide/service-worker-getting-started.md b/aio/content/guide/service-worker-getting-started.md index e622f895302f7..5d36fdf3cf5bf 100644 --- a/aio/content/guide/service-worker-getting-started.md +++ b/aio/content/guide/service-worker-getting-started.md @@ -1,7 +1,7 @@ # Getting started with service workers -This document explains how to enable Angular service worker support in projects that you created with the [Angular CLI](cli). It then uses a simple example to show you a service worker in action, demonstrating loading and basic caching. +This document explains how to enable Angular service worker support in projects that you created with the [Angular CLI](cli). It then uses a simple example to show you a service worker in action, demonstrating loading and basic caching. #### Prerequisites @@ -10,26 +10,26 @@ A basic understanding of the information in [Introduction to Angular service wor ## Adding a service worker to your project -To set up the Angular service worker in your project, use the CLI command `ng add @angular/pwa`. It takes care of configuring your app to use service workers by adding the `service-worker` package along +To set up the Angular service worker in your project, use the CLI command `ng add @angular/pwa`. It takes care of configuring your app to use service workers by adding the `service-worker` package along with setting up the necessary support files. ```sh -ng add @angular/pwa --project *project-name* +ng add @angular/pwa --project *project-name* ``` The above command completes the following actions: -1. Adds the `@angular/service-worker` package to your project. +1. Adds the `@angular/service-worker` package to your project. 2. Enables service worker build support in the CLI. 3. Imports and registers the service worker in the app module. 4. Updates the `index.html` file: * Includes a link to add the `manifest.json` file. * Adds meta tags for `theme-color`. 5. Installs icon files to support the installed Progressive Web App (PWA). -6. Creates the service worker configuration file called [`ngsw-config.json`](/guide/service-worker-config), which specifies the caching behaviors and other settings. +6. Creates the service worker configuration file called [`ngsw-config.json`](/guide/service-worker-config), which specifies the caching behaviors and other settings. - Now, build the project: + Now, build the project: ```sh ng build --prod @@ -40,8 +40,8 @@ The CLI project is now set up to use the Angular service worker. ## Service worker in action: a tour -This section demonstrates a service worker in action, -using an example application. +This section demonstrates a service worker in action, +using an example application. ### Serving with `http-server` @@ -61,7 +61,7 @@ With the server running, you can point your browser at http://localhost:8080/. Y ### Simulating a network issue -To simulate a network issue, disable network interaction for your application. In Chrome: +To simulate a network issue, disable network interaction for your application. In Chrome: 1. Select **Tools** > **Developer Tools** (from the Chrome menu located at the top right corner). 2. Go to the **Network tab**. @@ -73,9 +73,9 @@ To simulate a network issue, disable network interaction for your application. I Now the app has no access to network interaction. -For applications that do not use the Angular service worker, refreshing now would display Chrome's Internet disconnected page that says "There is no Internet connection". +For applications that do not use the Angular service worker, refreshing now would display Chrome's Internet disconnected page that says "There is no Internet connection". -With the addition of an Angular service worker, the application behavior changes. On a refresh, the page loads normally. +With the addition of an Angular service worker, the application behavior changes. On a refresh, the page loads normally. If you look at the Network tab, you can verify that the service worker is active. @@ -107,12 +107,12 @@ Pay attention to two key points: ### Making changes to your application -Now that you've seen how service workers cache your application, the -next step is understanding how updates work. +Now that you've seen how service workers cache your application, the +next step is understanding how updates work. 1. If you're testing in an incognito window, open a second blank tab. This will keep the incognito and the cache state alive during your test. -2. Close the application tab, but not the window. This should also close the Developer Tools. +2. Close the application tab, but not the window. This should also close the Developer Tools. 3. Shut down `http-server`. @@ -156,4 +156,4 @@ The service worker installed the updated version of your app *in the background* ## More on Angular service workers You may also be interested in the following: -* [Communicating with service workers](guide/service-worker-communications). +* [Communicating with service workers](guide/service-worker-communications). \ No newline at end of file diff --git a/packages/service-worker/src/module.ts b/packages/service-worker/src/module.ts index ddcd9102a43c8..5d07f9e88e74d 100644 --- a/packages/service-worker/src/module.ts +++ b/packages/service-worker/src/module.ts @@ -8,6 +8,7 @@ import {isPlatformBrowser} from '@angular/common'; import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, PLATFORM_ID} from '@angular/core'; +import {Observable} from 'rxjs'; import {filter, take} from 'rxjs/operators'; import {NgswCommChannel} from './low_level'; @@ -41,6 +42,8 @@ export abstract class SwRegistrationOptions { * [ServiceWorkerContainer#register()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register). */ scope?: string; + + registrationStrategy?: (() => Observable)|string; } export const SCRIPT = new InjectionToken('NGSW_REGISTER_SCRIPT'); @@ -68,7 +71,33 @@ export function ngswAppInitializer( // Don't return the Promise, as that will block the application until the SW is registered, and // cause a crash if the SW registration fails. - whenStable.then(() => navigator.serviceWorker.register(script, {scope: options.scope})); + if (typeof options.registrationStrategy === 'function') { + const observable = options.registrationStrategy(); + const subscription = observable.subscribe(() => { + navigator.serviceWorker.register(script, {scope: options.scope}); + subscription.unsubscribe(); + }); + } else { + const registrationStrategy = typeof options.registrationStrategy === 'string' ? + options.registrationStrategy : + 'registerWhenStable'; + if (registrationStrategy === 'registerWhenStable') { + whenStable.then(() => navigator.serviceWorker.register(script, {scope: options.scope})); + } else if (registrationStrategy === 'registerImmediately') { + navigator.serviceWorker.register(script, {scope: options.scope}); + } else if (registrationStrategy.indexOf('registerDelay') !== -1) { + const split = registrationStrategy.split(':'); + const delayStr = split.length > 1 ? split[1] : undefined; + const delay = Number(delayStr); + setTimeout( + () => navigator.serviceWorker.register(script, {scope: options.scope}), + typeof delay === 'number' ? delay : 0); + } else { + // wrong strategy + throw new Error( + `Unknown service worker registration strategy: ${options.registrationStrategy}`); + } + } }; return initializer; } diff --git a/tools/public_api_guard/service-worker/service-worker.d.ts b/tools/public_api_guard/service-worker/service-worker.d.ts index 80eddf9878f64..063b6fab9becc 100644 --- a/tools/public_api_guard/service-worker/service-worker.d.ts +++ b/tools/public_api_guard/service-worker/service-worker.d.ts @@ -1,5 +1,7 @@ + registrationStrategy?: (() => Observable) | string; export declare class ServiceWorkerModule { static register(script: string, opts?: SwRegistrationOptions): ModuleWithProviders; + registrationStrategy?: (() => Observable) | string; } export declare class SwPush { From f142987e43e24ba47bf6672fe105d53d2ad7bff9 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 25 Apr 2019 16:56:59 +0300 Subject: [PATCH 6/8] fixup! feat(service-worker): allow configuring when the SW is registered --- tools/public_api_guard/service-worker/service-worker.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/public_api_guard/service-worker/service-worker.d.ts b/tools/public_api_guard/service-worker/service-worker.d.ts index 063b6fab9becc..1ecb0bf761e07 100644 --- a/tools/public_api_guard/service-worker/service-worker.d.ts +++ b/tools/public_api_guard/service-worker/service-worker.d.ts @@ -1,7 +1,5 @@ - registrationStrategy?: (() => Observable) | string; export declare class ServiceWorkerModule { static register(script: string, opts?: SwRegistrationOptions): ModuleWithProviders; - registrationStrategy?: (() => Observable) | string; } export declare class SwPush { @@ -23,6 +21,7 @@ export declare class SwPush { export declare abstract class SwRegistrationOptions { enabled?: boolean; + registrationStrategy?: (() => Observable) | string; scope?: string; } From c6a21cce9b542ae9bb39b5e67e492fd9c948671b Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 7 Nov 2018 22:45:40 +0200 Subject: [PATCH 7/8] refactor(service-worker): DRY up SW registration logic --- aio/content/guide/service-worker-config.md | 12 --- packages/service-worker/src/module.ts | 83 ++++++++++++------- .../service-worker/service-worker.d.ts | 2 +- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/aio/content/guide/service-worker-config.md b/aio/content/guide/service-worker-config.md index 1cb77cfb561fc..c2273b28cac0d 100644 --- a/aio/content/guide/service-worker-config.md +++ b/aio/content/guide/service-worker-config.md @@ -218,15 +218,3 @@ If the field is omitted, it defaults to: '!/**/*__*/**', // Exclude URLs containing `__` in any other segment. ] ``` - -## `register options` - - - You can pass some options to the `register()` method. -- enabled: optional parameter, by default is true, if enabled is false, the module will behave like the browser not support service worker, and service worker will not be registered. -- scope: optional parameter, to specify the subset of your content that you want the service worker to control. -- registrationStrategy: optional parameter, specify a strategy that determines when to register the service worker, the available options are: - - registerWhenStable: this is the default behavior, the service worker will register when the application is stable (no microTasks or macroTasks remain). - - registerImmediately: register immediately without waiting the application to become stable. - - registerDelay:timeout : register after the timeout period, `timeout` is the number of milliseconds to delay registration. For example `registerDelay:5000` would register the service worker after 5 seconds. If the number of `timeout` is not given (`registerDelay`), by default, `timeout` will be `0`, but it is not equal to `registerImmediately`, it will still run a `setTimeout(register, 0)` to wait all `microTasks` to finish then perform registration of the service worker. - - A factory to get Observable : you can also specify a factory which returns an Observable, the service worker will be registered the first time that a value is emitted by the Observable. \ No newline at end of file diff --git a/packages/service-worker/src/module.ts b/packages/service-worker/src/module.ts index 5d07f9e88e74d..39e8f62e65f17 100644 --- a/packages/service-worker/src/module.ts +++ b/packages/service-worker/src/module.ts @@ -8,8 +8,8 @@ import {isPlatformBrowser} from '@angular/common'; import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, PLATFORM_ID} from '@angular/core'; -import {Observable} from 'rxjs'; -import {filter, take} from 'rxjs/operators'; +import {Observable, of } from 'rxjs'; +import {delay, filter, take} from 'rxjs/operators'; import {NgswCommChannel} from './low_level'; import {SwPush} from './push'; @@ -43,7 +43,32 @@ export abstract class SwRegistrationOptions { */ scope?: string; - registrationStrategy?: (() => Observable)|string; + /** + * Defines the ServiceWorker registration strategy, which determines when it will be registered + * with the browser. + * + * The default behavior of registering once the application stabilizes (i.e. as soon as there are + * no pending micro- and macro-tasks), is designed register the ServiceWorker as soon as possible + * but without affecting the application's first time load. + * + * Still, there might be cases where you want more control over when the ServiceWorker is + * registered (e.g. there might be a long-running timeout or polling interval, preventing the app + * to stabilize). The available option are: + * + * - `registerWhenStable`: Register as soon as the application stabilizes (no pending + * micro-/macro-tasks). + * - `registerImmediately`: Register immediately. + * - `registerWithDelay:`: Register with a delay of `` milliseconds. For + * example, use `registerWithDelay:5000` to register the ServiceWorker after 5 seconds. If + * `` is omitted, is defaults to `0`, which will register the ServiceWorker as soon + * as possible but still asynchronously, once all pending micro-tasks are completed. + * - An [Observable](guide/observables) factory function: A function that returns an `Observable`. + * The function will be used at runtime to obtain and subscribe to the `Observable` and the + * ServiceWorker will be registered as soon as the first value is emitted. + * + * Default: 'registerWhenStable' + */ + registrationStrategy?: string|(() => Observable); } export const SCRIPT = new InjectionToken('NGSW_REGISTER_SCRIPT'); @@ -52,13 +77,10 @@ export function ngswAppInitializer( injector: Injector, script: string, options: SwRegistrationOptions, platformId: string): Function { const initializer = () => { - const app = injector.get(ApplicationRef); if (!(isPlatformBrowser(platformId) && ('serviceWorker' in navigator) && options.enabled !== false)) { return; } - const whenStable = - app.isStable.pipe(filter((stable: boolean) => !!stable), take(1)).toPromise(); // Wait for service worker controller changes, and fire an INITIALIZE action when a new SW // becomes active. This allows the SW to initialize itself even if there is no application @@ -69,35 +91,34 @@ export function ngswAppInitializer( } }); - // Don't return the Promise, as that will block the application until the SW is registered, and - // cause a crash if the SW registration fails. + let readyToRegister$: Observable; + if (typeof options.registrationStrategy === 'function') { - const observable = options.registrationStrategy(); - const subscription = observable.subscribe(() => { - navigator.serviceWorker.register(script, {scope: options.scope}); - subscription.unsubscribe(); - }); + readyToRegister$ = options.registrationStrategy(); } else { - const registrationStrategy = typeof options.registrationStrategy === 'string' ? - options.registrationStrategy : - 'registerWhenStable'; - if (registrationStrategy === 'registerWhenStable') { - whenStable.then(() => navigator.serviceWorker.register(script, {scope: options.scope})); - } else if (registrationStrategy === 'registerImmediately') { - navigator.serviceWorker.register(script, {scope: options.scope}); - } else if (registrationStrategy.indexOf('registerDelay') !== -1) { - const split = registrationStrategy.split(':'); - const delayStr = split.length > 1 ? split[1] : undefined; - const delay = Number(delayStr); - setTimeout( - () => navigator.serviceWorker.register(script, {scope: options.scope}), - typeof delay === 'number' ? delay : 0); - } else { - // wrong strategy - throw new Error( - `Unknown service worker registration strategy: ${options.registrationStrategy}`); + const [strategy, ...args] = (options.registrationStrategy || 'registerWhenStable').split(':'); + switch (strategy) { + case 'registerImmediately': + readyToRegister$ = of (null); + break; + case 'registerWithDelay': + readyToRegister$ = of (null).pipe(delay(+args[0] || 0)); + break; + case 'registerWhenStable': + const appRef = injector.get(ApplicationRef); + readyToRegister$ = appRef.isStable.pipe(filter(stable => stable)); + break; + default: + // Unknown strategy. + throw new Error( + `Unknown ServiceWorker registration strategy: ${options.registrationStrategy}`); } } + + // Don't return anything to avoid blocking the application until the SW is registered or + // causing a crash if the SW registration fails. + readyToRegister$.pipe(take(1)).subscribe( + () => navigator.serviceWorker.register(script, {scope: options.scope})); }; return initializer; } diff --git a/tools/public_api_guard/service-worker/service-worker.d.ts b/tools/public_api_guard/service-worker/service-worker.d.ts index 1ecb0bf761e07..06a389a790eb2 100644 --- a/tools/public_api_guard/service-worker/service-worker.d.ts +++ b/tools/public_api_guard/service-worker/service-worker.d.ts @@ -21,7 +21,7 @@ export declare class SwPush { export declare abstract class SwRegistrationOptions { enabled?: boolean; - registrationStrategy?: (() => Observable) | string; + registrationStrategy?: string | (() => Observable); scope?: string; } From 5920d001ef46925e10c85e6d6046eabb369142bb Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 7 Nov 2018 22:46:22 +0200 Subject: [PATCH 8/8] test(service-worker): add tests for `RegistrationOptions#registrationStrategy` --- packages/service-worker/test/module_spec.ts | 160 +++++++++++++++++--- 1 file changed, 141 insertions(+), 19 deletions(-) diff --git a/packages/service-worker/test/module_spec.ts b/packages/service-worker/test/module_spec.ts index 14e1e86b099c2..aec5fe17846dc 100644 --- a/packages/service-worker/test/module_spec.ts +++ b/packages/service-worker/test/module_spec.ts @@ -7,7 +7,8 @@ */ import {ApplicationRef, PLATFORM_ID} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; +import {Subject} from 'rxjs'; import {filter, take} from 'rxjs/operators'; import {ServiceWorkerModule, SwRegistrationOptions} from '../src/module'; @@ -22,6 +23,11 @@ describe('ServiceWorkerModule', () => { let swRegisterSpy: jasmine.Spy; + const untilStable = () => { + const appRef: ApplicationRef = TestBed.get(ApplicationRef); + return appRef.isStable.pipe(filter(Boolean), take(1)).toPromise(); + }; + beforeEach(() => swRegisterSpy = spyOn(navigator.serviceWorker, 'register')); describe('register()', () => { @@ -31,8 +37,7 @@ describe('ServiceWorkerModule', () => { providers: [{provide: PLATFORM_ID, useValue: 'browser'}], }); - const appRef: ApplicationRef = TestBed.get(ApplicationRef); - await appRef.isStable.pipe(filter(Boolean), take(1)).toPromise(); + await untilStable(); }; it('sets the registration options', async() => { @@ -58,6 +63,7 @@ describe('ServiceWorkerModule', () => { it('defaults to enabling the SW', async() => { await configTestBed({}); + expect(TestBed.get(SwUpdate).isEnabled).toBe(true); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); }); @@ -65,45 +71,161 @@ describe('ServiceWorkerModule', () => { describe('SwRegistrationOptions', () => { const configTestBed = - async(providerOpts: SwRegistrationOptions, staticOpts?: SwRegistrationOptions) => { - TestBed.configureTestingModule({ - imports: [ServiceWorkerModule.register('sw.js', staticOpts || {scope: 'static'})], - providers: [ - {provide: PLATFORM_ID, useValue: 'browser'}, - {provide: SwRegistrationOptions, useFactory: () => providerOpts}, - ], - }); - - const appRef: ApplicationRef = TestBed.get(ApplicationRef); - await appRef.isStable.pipe(filter(Boolean), take(1)).toPromise(); - }; + (providerOpts: SwRegistrationOptions, staticOpts?: SwRegistrationOptions) => { + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('sw.js', staticOpts || {scope: 'static'})], + providers: [ + {provide: PLATFORM_ID, useValue: 'browser'}, + {provide: SwRegistrationOptions, useFactory: () => providerOpts}, + ], + }); + }; it('sets the registration options (and overwrites those set via `.register()`', async() => { - await configTestBed({enabled: true, scope: 'provider'}); + configTestBed({enabled: true, scope: 'provider'}); + await untilStable(); expect(TestBed.get(SwRegistrationOptions)).toEqual({enabled: true, scope: 'provider'}); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'}); }); it('can disable the SW', async() => { - await configTestBed({enabled: false}, {enabled: true}); + configTestBed({enabled: false}, {enabled: true}); + await untilStable(); expect(TestBed.get(SwUpdate).isEnabled).toBe(false); expect(swRegisterSpy).not.toHaveBeenCalled(); }); it('can enable the SW', async() => { - await configTestBed({enabled: true}, {enabled: false}); + configTestBed({enabled: true}, {enabled: false}); + await untilStable(); expect(TestBed.get(SwUpdate).isEnabled).toBe(true); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); }); it('defaults to enabling the SW', async() => { - await configTestBed({}, {enabled: false}); + configTestBed({}, {enabled: false}); + await untilStable(); expect(TestBed.get(SwUpdate).isEnabled).toBe(true); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); }); + + describe('registrationStrategy', () => { + const configTestBedWithMockedStability = + (strategy?: SwRegistrationOptions['registrationStrategy']) => { + const isStableSub = new Subject(); + + TestBed.configureTestingModule({ + imports: [ServiceWorkerModule.register('sw.js')], + providers: [ + {provide: ApplicationRef, useValue: {isStable: isStableSub.asObservable()}}, + {provide: PLATFORM_ID, useValue: 'browser'}, + { + provide: SwRegistrationOptions, + useFactory: () => ({registrationStrategy: strategy}) + }, + ], + }); + + // Dummy `get()` call to initialize the test "app". + TestBed.get(ApplicationRef); + + return isStableSub; + }; + + it('defaults to registering the SW when the app stabilizes', fakeAsync(() => { + const isStableSub = configTestBedWithMockedStability(); + + isStableSub.next(false); + isStableSub.next(false); + + tick(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + isStableSub.next(true); + + tick(); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW when the app stabilizes with `registerWhenStable`', fakeAsync(() => { + const isStableSub = configTestBedWithMockedStability('registerWhenStable'); + + isStableSub.next(false); + isStableSub.next(false); + + tick(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + isStableSub.next(true); + + tick(); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW immediatelly (synchronously) with `registerImmediately`', () => { + configTestBedWithMockedStability('registerImmediately'); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + }); + + it('registers the SW after the specified delay with `registerWithDelay:`', + fakeAsync(() => { + configTestBedWithMockedStability('registerWithDelay:100000'); + + tick(99999); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(1); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW asap (asynchronously) with `registerWithDelay:`', fakeAsync(() => { + configTestBedWithMockedStability('registerWithDelay:'); + + // Create a microtask. + Promise.resolve(); + + flushMicrotasks(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(0); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW asap (asynchronously) with `registerWithDelay`', fakeAsync(() => { + configTestBedWithMockedStability('registerWithDelay'); + + // Create a microtask. + Promise.resolve(); + + flushMicrotasks(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(0); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW on first emitted value with observable factory function', + fakeAsync(() => { + const registerSub = new Subject(); + const isStableSub = configTestBedWithMockedStability(() => registerSub.asObservable()); + + isStableSub.next(true); + tick(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + registerSub.next(); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('throws an error with unknown strategy', () => { + expect(() => configTestBedWithMockedStability('registerYesterday')) + .toThrowError('Unknown ServiceWorker registration strategy: registerYesterday'); + expect(swRegisterSpy).not.toHaveBeenCalled(); + }); + }); }); });