Skip to content

Commit 754a359

Browse files
authored
Merge pull request #61 from chrisjwalk/fix/sw-update-polling-59
fix: check for SW updates on NavigationEnd
2 parents 9323f3e + 992feaa commit 754a359

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

libs/shared/src/lib/state/sw-update.store.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { ApplicationRef } from '@angular/core';
12
import { TestBed } from '@angular/core/testing';
3+
import { NavigationEnd, Router } from '@angular/router';
24
import { MatSnackBar } from '@angular/material/snack-bar';
35
import { provideNoopAnimations } from '@angular/platform-browser/animations';
46
import { SwUpdate, VersionEvent } from '@angular/service-worker';
@@ -10,9 +12,11 @@ describe('SwUpdateStore', () => {
1012
let store: SwUpdateStore;
1113
let snackBar: MatSnackBar;
1214
let versionUpdatesSubject: Subject<VersionEvent>;
15+
let routerEventsSubject: Subject<NavigationEnd>;
1316

1417
function setup(isEnabled: boolean) {
1518
versionUpdatesSubject = new Subject<VersionEvent>();
19+
routerEventsSubject = new Subject<NavigationEnd>();
1620

1721
TestBed.configureTestingModule({
1822
providers: [
@@ -24,8 +28,13 @@ describe('SwUpdateStore', () => {
2428
isEnabled,
2529
versionUpdates: versionUpdatesSubject.asObservable(),
2630
activateUpdate: vi.fn().mockResolvedValue(undefined),
31+
checkForUpdate: vi.fn().mockResolvedValue(false),
2732
},
2833
},
34+
{
35+
provide: Router,
36+
useValue: { events: routerEventsSubject.asObservable() },
37+
},
2938
],
3039
});
3140

@@ -124,6 +133,51 @@ describe('SwUpdateStore', () => {
124133
expect(openSpy).not.toHaveBeenCalled();
125134
});
126135

136+
it('should call activateUpdate when snackbar action is clicked', async () => {
137+
const activateUpdateSpy = vi.spyOn(
138+
TestBed.inject(SwUpdate),
139+
'activateUpdate',
140+
);
141+
const actionSubject = new Subject<void>();
142+
vi.spyOn(snackBar, 'open').mockReturnValue({
143+
onAction: () => actionSubject.asObservable(),
144+
dismiss: vi.fn(),
145+
afterDismissed: () => EMPTY,
146+
afterOpened: () => EMPTY,
147+
instance: {} as never,
148+
containerInstance: {} as never,
149+
});
150+
151+
versionUpdatesSubject.next({
152+
type: 'VERSION_READY',
153+
currentVersion: { hash: 'abc', appData: null },
154+
latestVersion: { hash: 'def', appData: null },
155+
} as VersionEvent);
156+
157+
actionSubject.next();
158+
await TestBed.inject(ApplicationRef).whenStable();
159+
160+
expect(activateUpdateSpy).toHaveBeenCalled();
161+
});
162+
163+
it('should call checkForUpdate on NavigationEnd', () => {
164+
const checkSpy = vi.spyOn(TestBed.inject(SwUpdate), 'checkForUpdate');
165+
166+
routerEventsSubject.next(new NavigationEnd(1, '/', '/'));
167+
168+
expect(checkSpy).toHaveBeenCalledTimes(1);
169+
});
170+
171+
it('should not call checkForUpdate when SwUpdate is disabled', () => {
172+
TestBed.resetTestingModule();
173+
setup(false);
174+
const checkSpy = vi.spyOn(TestBed.inject(SwUpdate), 'checkForUpdate');
175+
176+
routerEventsSubject.next(new NavigationEnd(1, '/', '/'));
177+
178+
expect(checkSpy).not.toHaveBeenCalled();
179+
});
180+
127181
it('should not subscribe to versionUpdates when SwUpdate is disabled', () => {
128182
TestBed.resetTestingModule();
129183
setup(false);

libs/shared/src/lib/state/sw-update.store.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { computed, inject } from '@angular/core';
2+
import { DOCUMENT } from '@angular/common';
3+
import { NavigationEnd, Router } from '@angular/router';
24
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
35
import { SwUpdate, VersionEvent } from '@angular/service-worker';
46
import {
@@ -53,15 +55,18 @@ export function withSwUpdateFeature() {
5355
() => store.versionEvent?.type() === 'VERSION_READY',
5456
),
5557
})),
56-
withMethods(({ swUpdate }) => ({
58+
withMethods(({ swUpdate }, doc = inject(DOCUMENT)) => ({
5759
reloadAppOnAction: rxMethod<void>(
5860
pipe(
5961
tap(async () => {
6062
await swUpdate.activateUpdate();
61-
document.location.reload();
63+
doc.location.reload();
6264
}),
6365
),
6466
),
67+
checkForUpdate: rxMethod<unknown>(
68+
pipe(tap(() => swUpdate.checkForUpdate())),
69+
),
6570
})),
6671
withMethods(({ snackBar, ...store }) => ({
6772
versionUpdates: rxMethod<VersionEvent>(
@@ -86,16 +91,20 @@ export function withSwUpdateFeature() {
8691
* Service Worker Update Store
8792
*
8893
* Subscribes to SwUpdate.versionUpdates and prompts for reload when
89-
* a new version is available. Subscription is started in onInit hook.
94+
* a new version is available. Checks for updates on each NavigationEnd
95+
* so the prompt only appears while the user is actively using the app.
9096
* This just needs to be provided/injected into the main app component
9197
* to function.
9298
*/
9399
export const SwUpdateStore = signalStore(
94100
withSwUpdateFeature(),
95-
withHooks((store, swUpdate = inject(SwUpdate)) => ({
101+
withHooks((store, swUpdate = inject(SwUpdate), router = inject(Router)) => ({
96102
onInit() {
97103
if (swUpdate.isEnabled) {
98104
store.versionUpdates(swUpdate.versionUpdates);
105+
store.checkForUpdate(
106+
router.events.pipe(filter((e) => e instanceof NavigationEnd)),
107+
);
99108
}
100109
},
101110
})),

0 commit comments

Comments
 (0)