
Previously, when using the `registerWhenStable` ServiceWorker registration strategy (which is also the default) Angular would wait indefinitely for the [app to stabilize][1], before registering the ServiceWorker script. This could lead to a situation where the ServiceWorker would never be registered when there was a long-running task (such as an interval or recurring timeout). Such tasks can often be started by a 3rd-party dependency (beyond the developer's control or even without them realizing). In addition, this situation is particularly hard to detect, because the ServiceWorker is typically not used during development and on production builds a previous ServiceWorker instance might be already active. This commit enhances the `registerWhenStable` registration strategy by adding support for an optional `<timeout>` argument, which guarantees that the ServiceWorker will be registered when the timeout expires, even if the app has not stabilized yet. For example, with `registerWhenStable:5000` the ServiceWorker will be registered as soon as the app stabilizes or after 5 seconds if the app has not stabilized by then. Related to #34464. [1]: https://angular.io/api/core/ApplicationRef#is-stable-examples PR Close #35870
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
/**
|
|
* @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, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
|
|
import {Subject} from 'rxjs';
|
|
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;
|
|
|
|
const untilStable = () => {
|
|
const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
|
|
return appRef.isStable.pipe(filter(Boolean), take(1)).toPromise();
|
|
};
|
|
|
|
beforeEach(
|
|
() => swRegisterSpy =
|
|
spyOn(navigator.serviceWorker, 'register').and.returnValue(Promise.resolve()));
|
|
|
|
describe('register()', () => {
|
|
const configTestBed = async(opts: SwRegistrationOptions) => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ServiceWorkerModule.register('sw.js', opts)],
|
|
providers: [{provide: PLATFORM_ID, useValue: 'browser'}],
|
|
});
|
|
|
|
await untilStable();
|
|
};
|
|
|
|
it('sets the registration options', async() => {
|
|
await configTestBed({enabled: true, scope: 'foo'});
|
|
|
|
expect(TestBed.inject(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.inject(SwUpdate).isEnabled).toBe(false);
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('can enable the SW', async() => {
|
|
await configTestBed({enabled: true});
|
|
|
|
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
});
|
|
|
|
it('defaults to enabling the SW', async() => {
|
|
await configTestBed({});
|
|
|
|
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
});
|
|
|
|
it('catches and a logs registration errors', async() => {
|
|
const consoleErrorSpy = spyOn(console, 'error');
|
|
swRegisterSpy.and.returnValue(Promise.reject('no reason'));
|
|
|
|
await configTestBed({enabled: true, scope: 'foo'});
|
|
expect(consoleErrorSpy)
|
|
.toHaveBeenCalledWith('Service worker registration failed with:', 'no reason');
|
|
});
|
|
});
|
|
|
|
describe('SwRegistrationOptions', () => {
|
|
const configTestBed =
|
|
(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() => {
|
|
configTestBed({enabled: true, scope: 'provider'});
|
|
await untilStable();
|
|
|
|
expect(TestBed.inject(SwRegistrationOptions)).toEqual({enabled: true, scope: 'provider'});
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: 'provider'});
|
|
});
|
|
|
|
it('can disable the SW', async() => {
|
|
configTestBed({enabled: false}, {enabled: true});
|
|
await untilStable();
|
|
|
|
expect(TestBed.inject(SwUpdate).isEnabled).toBe(false);
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('can enable the SW', async() => {
|
|
configTestBed({enabled: true}, {enabled: false});
|
|
await untilStable();
|
|
|
|
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
});
|
|
|
|
it('defaults to enabling the SW', async() => {
|
|
configTestBed({}, {enabled: false});
|
|
await untilStable();
|
|
|
|
expect(TestBed.inject(SwUpdate).isEnabled).toBe(true);
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
});
|
|
|
|
describe('registrationStrategy', () => {
|
|
const configTestBedWithMockedStability =
|
|
(strategy?: SwRegistrationOptions['registrationStrategy']) => {
|
|
const isStableSub = new Subject<boolean>();
|
|
|
|
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 `inject()` call to initialize the test "app".
|
|
TestBed.inject(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();
|
|
|
|
tick(60000);
|
|
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:<timeout>`',
|
|
fakeAsync(() => {
|
|
const isStableSub = configTestBedWithMockedStability('registerWhenStable:1000');
|
|
|
|
isStableSub.next(false);
|
|
isStableSub.next(false);
|
|
|
|
tick();
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
tick(500);
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
isStableSub.next(true);
|
|
|
|
tick();
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
}));
|
|
|
|
it('registers the SW after `timeout` if the app does not stabilize with `registerWhenStable:<timeout>`',
|
|
fakeAsync(() => {
|
|
configTestBedWithMockedStability('registerWhenStable:1000');
|
|
|
|
tick(999);
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
tick(1);
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
}));
|
|
|
|
it('registers the SW asap (asynchronously) before the app stabilizes with `registerWhenStable:0`',
|
|
fakeAsync(() => {
|
|
const isStableSub = configTestBedWithMockedStability('registerWhenStable:0');
|
|
|
|
// Create a microtask.
|
|
Promise.resolve();
|
|
|
|
flushMicrotasks();
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
tick(0);
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
}));
|
|
|
|
it('registers the SW only when the app stabilizes with `registerWhenStable:`',
|
|
fakeAsync(() => {
|
|
const isStableSub = configTestBedWithMockedStability('registerWhenStable:');
|
|
|
|
isStableSub.next(false);
|
|
isStableSub.next(false);
|
|
|
|
tick();
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
tick(60000);
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
isStableSub.next(true);
|
|
|
|
tick();
|
|
expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined});
|
|
}));
|
|
|
|
it('registers the SW only when the app stabilizes with `registerWhenStable`',
|
|
fakeAsync(() => {
|
|
const isStableSub = configTestBedWithMockedStability('registerWhenStable');
|
|
|
|
isStableSub.next(false);
|
|
isStableSub.next(false);
|
|
|
|
tick();
|
|
expect(swRegisterSpy).not.toHaveBeenCalled();
|
|
|
|
tick(60000);
|
|
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:<delay>`',
|
|
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<void>();
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
});
|