feat(aio): enable activating available ServiceWorker updates (#15561)
This commit is contained in:
parent
67719f2185
commit
3ccbe28d9f
|
@ -17,6 +17,8 @@ import { LocationService } from 'app/shared/location.service';
|
|||
import { MockLocationService } from 'testing/location.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
|
||||
import { MockSwUpdateNotificationsService } from 'testing/sw-update-notifications.service';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
|
@ -32,7 +34,8 @@ describe('AppComponent', () => {
|
|||
{ provide: Http, useClass: TestHttp },
|
||||
{ provide: LocationService, useFactory: () => new MockLocationService(initialUrl) },
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: SearchService, useClass: MockSearchService }
|
||||
{ provide: SearchService, useClass: MockSearchService },
|
||||
{ provide: SwUpdateNotificationsService, useClass: MockSwUpdateNotificationsService },
|
||||
]
|
||||
});
|
||||
TestBed.compileComponents();
|
||||
|
@ -48,6 +51,13 @@ describe('AppComponent', () => {
|
|||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
describe('ServiceWorker update notifications', () => {
|
||||
it('should be enabled', () => {
|
||||
const swUpdateNotifications = TestBed.get(SwUpdateNotificationsService) as SwUpdateNotificationsService;
|
||||
expect(swUpdateNotifications.enable).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('is Hamburger Visible', () => {
|
||||
console.log('PENDING: AppComponent');
|
||||
});
|
||||
|
|
|
@ -9,6 +9,8 @@ import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
|||
import { LocationService } from 'app/shared/location.service';
|
||||
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
|
||||
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
|
||||
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
|
||||
|
||||
|
||||
const sideNavView = 'SideNav';
|
||||
|
||||
|
@ -54,7 +56,8 @@ export class AppComponent implements OnInit {
|
|||
private autoScrollService: AutoScrollService,
|
||||
private documentService: DocumentService,
|
||||
private locationService: LocationService,
|
||||
private navigationService: NavigationService
|
||||
private navigationService: NavigationService,
|
||||
private swUpdateNotifications: SwUpdateNotificationsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -83,6 +86,8 @@ export class AppComponent implements OnInit {
|
|||
|
||||
this.navigationService.versionInfo.subscribe( vi => this.versionInfo = vi );
|
||||
|
||||
this.swUpdateNotifications.enable();
|
||||
|
||||
this.onResize(window.innerWidth);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ import { Platform } from '@angular/material/core';
|
|||
// crashes with "missing first" operator when SideNav.mode is "over"
|
||||
import 'rxjs/add/operator/first';
|
||||
|
||||
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
|
||||
|
||||
import { AppComponent } from 'app/app.component';
|
||||
import { ApiService } from 'app/embedded/api/api.service';
|
||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||
|
@ -46,7 +48,8 @@ import { AutoScrollService } from 'app/shared/auto-scroll.service';
|
|||
MdInputModule,
|
||||
MdToolbarModule,
|
||||
MdSidenavModule,
|
||||
MdTabsModule
|
||||
MdTabsModule,
|
||||
SwUpdatesModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { fakeAsync, flushMicrotasks } from '@angular/core/testing';
|
||||
import { MdSnackBar, MdSnackBarConfig } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { MockSwUpdatesService } from 'testing/sw-updates.service';
|
||||
import { SwUpdateNotificationsService } from './sw-update-notifications.service';
|
||||
import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
describe('SwUpdateNotificationsService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let service: SwUpdateNotificationsService;
|
||||
let swUpdates: MockSwUpdatesService;
|
||||
let snackBar: MockMdSnackBar;
|
||||
|
||||
// Helpers
|
||||
const activateUpdate = success => {
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
snackBar.$$lastRef.$$onActionSubj.next();
|
||||
swUpdates.$$activateUpdateSubj.next(success);
|
||||
|
||||
flushMicrotasks();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: MdSnackBar, useClass: MockMdSnackBar },
|
||||
{ provide: SwUpdatesService, useClass: MockSwUpdatesService },
|
||||
SwUpdateNotificationsService
|
||||
]);
|
||||
service = injector.get(SwUpdateNotificationsService);
|
||||
swUpdates = injector.get(SwUpdatesService);
|
||||
snackBar = injector.get(MdSnackBar);
|
||||
});
|
||||
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not notify about available updates before being enabled', () => {
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
expect(snackBar.$$lastRef).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('when enabled', () => {
|
||||
beforeEach(() => service.enable());
|
||||
|
||||
|
||||
it('should not re-subscribe to updates if already enabled', () => {
|
||||
spyOn(snackBar, 'open').and.callThrough();
|
||||
|
||||
service.enable();
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
|
||||
expect(snackBar.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should notify when updates are available', () => {
|
||||
expect(snackBar.$$lastRef).toBeUndefined();
|
||||
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
|
||||
expect(snackBar.$$lastRef.$$message).toContain('ServiceWorker update available');
|
||||
expect(snackBar.$$lastRef.$$action).toBe('Activate');
|
||||
expect(snackBar.$$lastRef.$$config.duration).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not notify when updates are not available', () => {
|
||||
swUpdates.$$isUpdateAvailableSubj.next(false);
|
||||
expect(snackBar.$$lastRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should activate the update when clicking on `Activate`', () => {
|
||||
spyOn(swUpdates, 'activateUpdate').and.callThrough();
|
||||
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
expect(swUpdates.activateUpdate).not.toHaveBeenCalled();
|
||||
|
||||
snackBar.$$lastRef.$$onActionSubj.next();
|
||||
expect(swUpdates.activateUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should report a successful activation', fakeAsync(() => {
|
||||
activateUpdate(true);
|
||||
|
||||
expect(snackBar.$$lastRef.$$message).toContain('Update activated successfully');
|
||||
expect(snackBar.$$lastRef.$$action).toBeNull();
|
||||
expect(snackBar.$$lastRef.$$config.duration).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('should report a failed activation', fakeAsync(() => {
|
||||
activateUpdate(false);
|
||||
|
||||
expect(snackBar.$$lastRef.$$message).toContain('Update activation failed');
|
||||
expect(snackBar.$$lastRef.$$action).toBe('Dismiss');
|
||||
expect(snackBar.$$lastRef.$$config.duration).toBeGreaterThan(0);
|
||||
}));
|
||||
|
||||
it('should dismiss the failed activation snack-bar when clicking on `Dismiss`', fakeAsync(() => {
|
||||
activateUpdate(false);
|
||||
expect(snackBar.$$lastRef.$$dismissed).toBe(false);
|
||||
|
||||
snackBar.$$lastRef.$$onActionSubj.next();
|
||||
expect(snackBar.$$lastRef.$$dismissed).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#disable()', () => {
|
||||
beforeEach(() => service.enable());
|
||||
|
||||
|
||||
it('should dismiss open update notification', () => {
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
expect(snackBar.$$lastRef.$$message).toContain('ServiceWorker update available');
|
||||
expect(snackBar.$$lastRef.$$dismissed).toBe(false);
|
||||
|
||||
service.disable();
|
||||
expect(snackBar.$$lastRef.$$dismissed).toBe(true);
|
||||
});
|
||||
|
||||
it('should dismiss open activation notification', fakeAsync(() => {
|
||||
activateUpdate(true);
|
||||
expect(snackBar.$$lastRef.$$message).toContain('Update activated successfully');
|
||||
expect(snackBar.$$lastRef.$$dismissed).toBe(false);
|
||||
|
||||
service.disable();
|
||||
expect(snackBar.$$lastRef.$$dismissed).toBe(true);
|
||||
}));
|
||||
|
||||
it('should ignore further updates', () => {
|
||||
service.disable();
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
|
||||
expect(snackBar.$$lastRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not ignore further updates if re-enabled', () => {
|
||||
service.disable();
|
||||
service.enable();
|
||||
expect(snackBar.$$lastRef).toBeUndefined();
|
||||
|
||||
swUpdates.$$isUpdateAvailableSubj.next(true);
|
||||
expect(snackBar.$$lastRef.$$message).toContain('ServiceWorker update available');
|
||||
});
|
||||
|
||||
it('should not ignore pending updates if re-enabled', () => {
|
||||
service.disable();
|
||||
swUpdates.isUpdateAvailable = Observable.of(true);
|
||||
expect(snackBar.$$lastRef).toBeUndefined();
|
||||
|
||||
service.enable();
|
||||
expect(snackBar.$$lastRef.$$message).toContain('ServiceWorker update available');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Mocks
|
||||
class MockMdSnackBarRef {
|
||||
$$afterDismissedSubj = new Subject();
|
||||
$$onActionSubj = new Subject();
|
||||
$$dismissed = false;
|
||||
|
||||
constructor(public $$message: string,
|
||||
public $$action: string,
|
||||
public $$config: MdSnackBarConfig) {}
|
||||
|
||||
afterDismissed() {
|
||||
return this.$$afterDismissedSubj;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$$dismissed = true;
|
||||
}
|
||||
|
||||
onAction() {
|
||||
return this.$$onActionSubj;
|
||||
}
|
||||
}
|
||||
|
||||
class MockMdSnackBar {
|
||||
$$lastRef: MockMdSnackBarRef;
|
||||
|
||||
open(message: string, action: string = null, config: MdSnackBarConfig = {}): MockMdSnackBarRef {
|
||||
if (this.$$lastRef && !this.$$lastRef.$$dismissed) {
|
||||
this.$$lastRef.dismiss();
|
||||
}
|
||||
|
||||
return this.$$lastRef = new MockMdSnackBarRef(message, action, config);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { MdSnackBar, MdSnackBarConfig, MdSnackBarRef } from '@angular/material';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import 'rxjs/add/operator/filter';
|
||||
|
||||
import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
/**
|
||||
* SwUpdateNotificationsService
|
||||
*
|
||||
* @description
|
||||
* Once enabled:
|
||||
* 1. Subscribes to ServiceWorker updates and prompts the user to activate.
|
||||
* 2. When the user confirms, it activates the update and notifies the user (upon activation success
|
||||
* or failure).
|
||||
* 3. Continues to listen for available ServiceWorker updates.
|
||||
*
|
||||
* @method
|
||||
* `disable()` {() => void} - Dismiss any open notifications and stop listening for ServiceWorker
|
||||
* updates.
|
||||
*
|
||||
* @method
|
||||
* `enable()` {() => void} - Start listening for ServiceWorker updates.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SwUpdateNotificationsService {
|
||||
private onDisable = new Subject();
|
||||
private snackBars: MdSnackBarRef<any>[] = [];
|
||||
private enabled = false;
|
||||
|
||||
constructor(private snackBarService: MdSnackBar, private swUpdates: SwUpdatesService) {
|
||||
this.onDisable.subscribe(() => this.snackBars.forEach(sb => sb.dismiss()));
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this.enabled) {
|
||||
this.enabled = false;
|
||||
this.onDisable.next();
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
this.swUpdates.isUpdateAvailable
|
||||
.filter(v => v)
|
||||
.takeUntil(this.onDisable)
|
||||
.subscribe(() => this.notifyForUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
private activateUpdate() {
|
||||
this.swUpdates.activateUpdate().then(success => {
|
||||
if (success) {
|
||||
this.onActivateSuccess();
|
||||
} else {
|
||||
this.onActivateFailure();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private notifyForUpdate() {
|
||||
this.openSnackBar('ServiceWorker update available.', 'Activate')
|
||||
.onAction().subscribe(() => this.activateUpdate());
|
||||
}
|
||||
|
||||
private onActivateFailure() {
|
||||
const snackBar = this.openSnackBar('Update activation failed :(', 'Dismiss', {duration: 5000});
|
||||
snackBar.onAction().subscribe(() => snackBar.dismiss());
|
||||
}
|
||||
|
||||
private onActivateSuccess() {
|
||||
this.openSnackBar('Update activated successfully! Reload the page to see the latest content.');
|
||||
}
|
||||
|
||||
private openSnackBar(message: string, action?: string, config?: MdSnackBarConfig): MdSnackBarRef<any> {
|
||||
const snackBar = this.snackBarService.open(message, action, config);
|
||||
snackBar.afterDismissed().subscribe(() => this.snackBars = this.snackBars.filter(sb => sb !== snackBar));
|
||||
|
||||
this.snackBars.push(snackBar);
|
||||
|
||||
return snackBar;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { MdSnackBarModule } from '@angular/material';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
|
||||
import { SwUpdateNotificationsService } from './sw-update-notifications.service';
|
||||
import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
MdSnackBarModule,
|
||||
ServiceWorkerModule
|
||||
],
|
||||
providers: [
|
||||
SwUpdateNotificationsService,
|
||||
SwUpdatesService
|
||||
]
|
||||
})
|
||||
export class SwUpdatesModule {}
|
|
@ -0,0 +1,218 @@
|
|||
import { ReflectiveInjector } from '@angular/core';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { NgServiceWorker } from '@angular/service-worker';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import 'rxjs/add/operator/take';
|
||||
|
||||
import { SwUpdatesService } from './sw-updates.service';
|
||||
|
||||
|
||||
describe('SwUpdatesService', () => {
|
||||
let injector: ReflectiveInjector;
|
||||
let service: SwUpdatesService;
|
||||
let sw: MockNgServiceWorker;
|
||||
let checkInterval: number;
|
||||
|
||||
// Helpers
|
||||
// NOTE:
|
||||
// Because `SwUpdatesService` uses the `debounceTime` operator, it needs to be instantiated
|
||||
// inside the `fakeAsync` zone (when `fakeAsync` is used for the test). Thus, we can't run
|
||||
// `setup()` in a `beforeEach()` block. We use the `run()` helper to call it inside each test's
|
||||
// zone.
|
||||
const setup = () => {
|
||||
injector = ReflectiveInjector.resolveAndCreate([
|
||||
{ provide: NgServiceWorker, useClass: MockNgServiceWorker },
|
||||
SwUpdatesService
|
||||
]);
|
||||
service = injector.get(SwUpdatesService);
|
||||
sw = injector.get(NgServiceWorker);
|
||||
checkInterval = (service as any).checkInterval;
|
||||
};
|
||||
const tearDown = () => service.ngOnDestroy();
|
||||
const run = specFn => () => {
|
||||
setup();
|
||||
specFn();
|
||||
tearDown();
|
||||
};
|
||||
|
||||
|
||||
it('should create', run(() => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should immediatelly check for updates when instantiated', run(() => {
|
||||
expect(sw.checkForUpdate).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should schedule a new check if there is no update available', fakeAsync(run(() => {
|
||||
sw.checkForUpdate.calls.reset();
|
||||
|
||||
sw.$$checkForUpdateSubj.next(false);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
|
||||
tick(checkInterval);
|
||||
expect(sw.checkForUpdate).toHaveBeenCalled();
|
||||
})));
|
||||
|
||||
it('should not schedule a new check if there is an update available', fakeAsync(run(() => {
|
||||
sw.checkForUpdate.calls.reset();
|
||||
|
||||
sw.$$checkForUpdateSubj.next(true);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
|
||||
tick(checkInterval);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
})));
|
||||
|
||||
describe('#activateUpdate()', () => {
|
||||
it('should return a promise', run(() => {
|
||||
expect(service.activateUpdate()).toEqual(jasmine.any(Promise));
|
||||
}));
|
||||
|
||||
it('should call `NgServiceWorker.activateUpdate()`', run(() => {
|
||||
expect(sw.activateUpdate).not.toHaveBeenCalled();
|
||||
|
||||
service.activateUpdate();
|
||||
expect(sw.activateUpdate).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not pass a specific version to `NgServiceWorker.activateUpdate()`', run(() => {
|
||||
(service.activateUpdate as Function)('foo');
|
||||
expect(sw.activateUpdate).toHaveBeenCalledWith(null);
|
||||
}));
|
||||
|
||||
it('should resolve the promise with the activation outcome', fakeAsync(run(() => {
|
||||
let outcome;
|
||||
|
||||
service.activateUpdate().then(v => outcome = v);
|
||||
sw.$$activateUpdateSubj.next(true);
|
||||
tick();
|
||||
expect(outcome).toBe(true);
|
||||
|
||||
service.activateUpdate().then(v => outcome = v);
|
||||
sw.$$activateUpdateSubj.next(false);
|
||||
tick();
|
||||
expect(outcome).toBe(false);
|
||||
})));
|
||||
|
||||
it('should schedule a new check (if the activation succeeded)', fakeAsync(run(() => {
|
||||
sw.checkForUpdate.calls.reset();
|
||||
|
||||
service.activateUpdate();
|
||||
|
||||
tick(checkInterval);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
|
||||
sw.$$activateUpdateSubj.next(true);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
|
||||
tick(checkInterval);
|
||||
expect(sw.checkForUpdate).toHaveBeenCalled();
|
||||
})));
|
||||
|
||||
it('should schedule a new check (if the activation failed)', fakeAsync(run(() => {
|
||||
sw.checkForUpdate.calls.reset();
|
||||
|
||||
service.activateUpdate();
|
||||
|
||||
tick(checkInterval);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
|
||||
sw.$$activateUpdateSubj.next(false);
|
||||
expect(sw.checkForUpdate).not.toHaveBeenCalled();
|
||||
|
||||
tick(checkInterval);
|
||||
expect(sw.checkForUpdate).toHaveBeenCalled();
|
||||
})));
|
||||
});
|
||||
|
||||
describe('#isUpdateAvailable', () => {
|
||||
let emittedValues: boolean[];
|
||||
|
||||
// Helpers
|
||||
const withSubscription = specFn => () => {
|
||||
emittedValues = [];
|
||||
service.isUpdateAvailable.subscribe(v => emittedValues.push(v));
|
||||
specFn();
|
||||
};
|
||||
|
||||
|
||||
it('should emit `false/true` when there is/isn\'t an update available',
|
||||
fakeAsync(run(withSubscription(() => {
|
||||
expect(emittedValues).toEqual([]);
|
||||
|
||||
sw.$$checkForUpdateSubj.next(false);
|
||||
expect(emittedValues).toEqual([false]);
|
||||
|
||||
tick(checkInterval);
|
||||
sw.$$checkForUpdateSubj.next(true);
|
||||
expect(emittedValues).toEqual([false, true]);
|
||||
})))
|
||||
);
|
||||
|
||||
it('should emit only when the value has changed',
|
||||
fakeAsync(run(withSubscription(() => {
|
||||
expect(emittedValues).toEqual([]);
|
||||
|
||||
sw.$$checkForUpdateSubj.next(false);
|
||||
expect(emittedValues).toEqual([false]);
|
||||
|
||||
tick(checkInterval);
|
||||
sw.$$checkForUpdateSubj.next(false);
|
||||
expect(emittedValues).toEqual([false]);
|
||||
|
||||
tick(checkInterval);
|
||||
sw.$$checkForUpdateSubj.next(false);
|
||||
expect(emittedValues).toEqual([false]);
|
||||
})))
|
||||
);
|
||||
|
||||
it('should emit `false` after a successful activation',
|
||||
fakeAsync(run(withSubscription(() => {
|
||||
sw.$$checkForUpdateSubj.next(true);
|
||||
expect(emittedValues).toEqual([true]);
|
||||
|
||||
service.activateUpdate();
|
||||
sw.$$activateUpdateSubj.next(true);
|
||||
|
||||
expect(emittedValues).toEqual([true, false]);
|
||||
})))
|
||||
);
|
||||
|
||||
it('should emit `false` after a failed activation',
|
||||
fakeAsync(run(withSubscription(() => {
|
||||
sw.$$checkForUpdateSubj.next(true);
|
||||
expect(emittedValues).toEqual([true]);
|
||||
|
||||
service.activateUpdate();
|
||||
sw.$$activateUpdateSubj.next(false);
|
||||
|
||||
expect(emittedValues).toEqual([true, false]);
|
||||
})))
|
||||
);
|
||||
|
||||
it('should emit not emit a vew value after activation if already `false`',
|
||||
fakeAsync(run(withSubscription(() => {
|
||||
sw.$$checkForUpdateSubj.next(false);
|
||||
expect(emittedValues).toEqual([false]);
|
||||
|
||||
service.activateUpdate();
|
||||
sw.$$activateUpdateSubj.next(true);
|
||||
|
||||
expect(emittedValues).toEqual([false]);
|
||||
})))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Mocks
|
||||
class MockNgServiceWorker {
|
||||
$$activateUpdateSubj = new Subject<boolean>();
|
||||
$$checkForUpdateSubj = new Subject<boolean>();
|
||||
|
||||
activateUpdate = jasmine.createSpy('MockNgServiceWorker.activateUpdate')
|
||||
.and.callFake(() => this.$$activateUpdateSubj.take(1));
|
||||
|
||||
checkForUpdate = jasmine.createSpy('MockNgServiceWorker.checkForUpdate')
|
||||
.and.callFake(() => this.$$checkForUpdateSubj.take(1));
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NgServiceWorker } from '@angular/service-worker';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import 'rxjs/add/operator/debounceTime';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
import 'rxjs/add/operator/concat';
|
||||
import 'rxjs/add/operator/startWith';
|
||||
import 'rxjs/add/operator/take';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
|
||||
/**
|
||||
* SwUpdatesService
|
||||
*
|
||||
* @description
|
||||
* 1. Checks for available ServiceWorker updates once instantiated.
|
||||
* 2. As long as there is no update available, re-checks every 6 hours.
|
||||
* 3. As soon as an update is detected, it waits until the update is activated, then starts checking
|
||||
* again (every 6 hours).
|
||||
*
|
||||
* @property
|
||||
* `isUpdateAvailable` {Observable<boolean>} - Emit `true`/`false` to indicate updates being
|
||||
* available or not. Remembers the last emitted value. Will only emit a new value if it is different
|
||||
* than the last one.
|
||||
*
|
||||
* @method
|
||||
* `activateUpdate()` {() => Promise<boolean>} - Activate the latest available update. The returned
|
||||
* promise resolves to `true` if an update was activated successfully and `false` if the activation
|
||||
* failed (e.g. if there was no update to activate).
|
||||
*/
|
||||
@Injectable()
|
||||
export class SwUpdatesService implements OnDestroy {
|
||||
private checkInterval = 1000 * 60 * 60 * 6; // 6 hours
|
||||
private onDestroy = new Subject();
|
||||
private checkForUpdateSubj = new Subject();
|
||||
private isUpdateAvailableSubj = new ReplaySubject<boolean>(1);
|
||||
isUpdateAvailable = this.isUpdateAvailableSubj.distinctUntilChanged();
|
||||
|
||||
constructor(private sw: NgServiceWorker) {
|
||||
this.checkForUpdateSubj
|
||||
.debounceTime(this.checkInterval)
|
||||
.takeUntil(this.onDestroy)
|
||||
.startWith(null)
|
||||
.subscribe(() => this.checkForUpdate());
|
||||
|
||||
this.isUpdateAvailableSubj
|
||||
.filter(v => !v)
|
||||
.takeUntil(this.onDestroy)
|
||||
.subscribe(() => this.checkForUpdateSubj.next());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
}
|
||||
|
||||
activateUpdate(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
this.sw.activateUpdate(null)
|
||||
// Temp workaround for https://github.com/angular/mobile-toolkit/pull/137.
|
||||
// TODO (gkalpak): Remove once #137 is fixed.
|
||||
.concat(Observable.of(false)).take(1)
|
||||
.do(() => this.isUpdateAvailableSubj.next(false))
|
||||
.subscribe(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private checkForUpdate() {
|
||||
this.sw.checkForUpdate()
|
||||
// Temp workaround for https://github.com/angular/mobile-toolkit/pull/137.
|
||||
// TODO (gkalpak): Remove once #137 is fixed.
|
||||
.concat(Observable.of(false)).take(1)
|
||||
.subscribe(v => this.isUpdateAvailableSubj.next(v));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export class MockSwUpdateNotificationsService {
|
||||
enable = jasmine.createSpy('MockSwUpdateNotificationsService.enable');
|
||||
disable = jasmine.createSpy('MockSwUpdateNotificationsService.disable');
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Subject } from 'rxjs/Subject';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
import 'rxjs/add/operator/take';
|
||||
|
||||
|
||||
export class MockSwUpdatesService {
|
||||
$$activateUpdateSubj = new Subject<boolean>();
|
||||
$$isUpdateAvailableSubj = new Subject<boolean>();
|
||||
isUpdateAvailable = this.$$isUpdateAvailableSubj.distinctUntilChanged();
|
||||
|
||||
activateUpdate(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
this.$$activateUpdateSubj
|
||||
// Better simulate what actually happens with the real ServiceWorker.
|
||||
.take(1)
|
||||
.do(() => this.$$isUpdateAvailableSubj.next(false))
|
||||
.subscribe(resolve);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue