feat(service-worker): handle 'notificationclick' events (#25860)

The previous version did not support the 'notificationclick' event.
Add event handler for the event and provide an observable of
clicked notifications in the SwPush service.

Closes #20956, #22311

PR Close #25860
This commit is contained in:
Joost Zoellner 2018-09-07 14:56:40 +02:00 committed by Kara Erickson
parent ea0a99610d
commit cf6ea283bb
8 changed files with 80 additions and 0 deletions

View File

@ -25,6 +25,8 @@ export class SwPush {
*/
readonly messages: Observable<object>;
readonly messagesClicked: Observable<object>;
/**
* Emits the currently active
* [PushSubscription](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription)
@ -45,12 +47,16 @@ export class SwPush {
constructor(private sw: NgswCommChannel) {
if (!sw.isEnabled) {
this.messages = NEVER;
this.messagesClicked = NEVER;
this.subscription = NEVER;
return;
}
this.messages = this.sw.eventsOfType<PushEvent>('PUSH').pipe(map(message => message.data));
this.messagesClicked =
this.sw.eventsOfType('NOTIFICATION_CLICK').pipe(map((message: any) => message.data));
this.pushManager = this.sw.registration.pipe(map(registration => registration.pushManager));
const workerDrivenSubscriptions = this.pushManager.pipe(switchMap(pm => pm.getSubscription()));

View File

@ -303,6 +303,26 @@ import {async_fit, async_it} from './async';
]);
});
});
describe('messagesClicked', () => {
it('receives notification clicked messages', () => {
const sendMessage = (type: string, message: string) =>
mock.sendMessage({type, data: {message}});
const receivedMessages: string[] = [];
push.messagesClicked.subscribe(
(msg: {message: string}) => receivedMessages.push(msg.message));
sendMessage('NOTIFICATION_CLICK', 'this was a click');
sendMessage('NOT_OTIFICATION_CLICK', 'this was not a click');
sendMessage('NOTIFICATION_CLICK', 'this was a click too');
sendMessage('KCILC_NOITACIFITON', 'this was a KCILC_NOITACIFITON message');
expect(receivedMessages).toEqual([
'this was a click',
'this was a click too',
]);
});
});
describe('subscription', () => {
let nextSubEmitResolve: () => void;
@ -367,6 +387,7 @@ import {async_fit, async_it} from './async';
it('does not crash on subscription to observables', () => {
push.messages.toPromise().catch(err => fail(err));
push.messagesClicked.toPromise().catch(err => fail(err));
push.subscription.toPromise().catch(err => fail(err));
});

View File

@ -88,6 +88,7 @@ const serverUpdate =
scope.clients.getMock('default') !.queue.subscribe(msg => { mock.sendMessage(msg); });
mock.messages.subscribe(msg => { scope.handleMessage(msg, 'default'); });
mock.messagesClicked.subscribe(msg => { scope.handleMessage(msg, 'default'); });
mock.setupSw();
reg = mock.mockRegistration !;
@ -128,5 +129,18 @@ const serverUpdate =
});
await gotPushNotice;
});
async_it('receives push message click events', async() => {
const push = new SwPush(comm);
scope.updateServerState(serverUpdate);
const gotNotificationClick = (async() => {
const event = await obsToSinglePromise(push.messagesClicked);
expect(event).toEqual({action: 'clicked', notification: {clicked: true}});
})();
await scope.handleClick({clicked: true}, 'clicked');
await gotNotificationClick;
});
});
})();

View File

@ -28,6 +28,7 @@ export class MockServiceWorkerContainer {
mockRegistration: MockServiceWorkerRegistration|null = null;
controller: MockServiceWorker|null = null;
messages = new Subject();
messagesClicked = new Subject();
addEventListener(event: 'controllerchange'|'message', handler: Function) {
if (event === 'controllerchange') {

View File

@ -159,6 +159,7 @@ export class Driver implements Debuggable, UpdateSource {
this.scope.addEventListener('fetch', (event) => this.onFetch(event !));
this.scope.addEventListener('message', (event) => this.onMessage(event !));
this.scope.addEventListener('push', (event) => this.onPush(event !));
this.scope.addEventListener('notificationclick', (event) => this.onClick(event !));
// The debugger generates debug pages in response to debugging requests.
this.debugger = new DebugHandler(this, this.adapter);
@ -275,6 +276,11 @@ export class Driver implements Debuggable, UpdateSource {
msg.waitUntil(this.handlePush(msg.data.json()));
}
private onClick(event: NotificationClickEvent): void {
// Handle the click event and keep the SW alive until it's handled.
event.waitUntil(this.handleClick(event.notification, event.action));
}
private async handleMessage(msg: MsgAny&{action: string}, from: Client): Promise<void> {
if (isMsgCheckForUpdates(msg)) {
const action = (async() => { await this.checkForUpdate(); })();
@ -299,6 +305,13 @@ export class Driver implements Debuggable, UpdateSource {
await this.scope.registration.showNotification(desc['title'] !, options);
}
private async handleClick(notification: Notification, action?: string): Promise<void> {
await this.broadcast({
type: 'NOTIFICATION_CLICK',
data: {action, notification},
});
}
private async reportStatus(client: Client, promise: Promise<void>, nonce: number): Promise<void> {
const response = {type: 'STATUS', nonce, status: true};
try {

View File

@ -83,6 +83,8 @@ interface PushEvent extends ExtendableEvent {
data: PushMessageData;
}
interface NotificationClickEvent extends NotificationEvent, ExtendableEvent {}
interface PushMessageData {
arrayBuffer(): ArrayBuffer;
blob(): Blob;
@ -114,6 +116,7 @@ interface ServiceWorkerGlobalScope {
addEventListener(event: 'fetch', fn: (event?: FetchEvent) => any): void;
addEventListener(event: 'install', fn: (event?: ExtendableEvent) => any): void;
addEventListener(event: 'push', fn: (event?: PushEvent) => any): void;
addEventListener(event: 'notificationclick', fn: (event?: NotificationClickEvent) => any): void;
addEventListener(event: 'sync', fn: (event?: SyncEvent) => any): void;
fetch(request: Request|string): Promise<Response>;

View File

@ -589,6 +589,16 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
}]);
});
async_it('broadcasts notification click events', async() => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
await scope.handleClick({title: 'This is a test', body: 'Test body'}, 'button');
expect(scope.clients.getMock('default') !.messages).toEqual([{
type: 'NOTIFICATION_CLICK',
data: {action: 'button', notification: {title: 'This is a test', body: 'Test body'}}
}]);
});
async_it('prefetches updates to lazy cache when set', async() => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;

View File

@ -228,6 +228,15 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
return event.ready;
}
handleClick(notification: Object, action?: string): Promise<void> {
if (!this.eventHandlers.has('notificationclick')) {
throw new Error('No notificationclick handler registered');
}
const event = new MockNotificationClickEvent(notification, action);
this.eventHandlers.get('notificationclick') !.call(this, event);
return event.ready;
}
timeout(ms: number): Promise<void> {
const promise = new Promise<void>(resolve => {
this.timers.push({
@ -340,6 +349,9 @@ class MockPushEvent extends MockExtendableEvent {
json: () => this._data,
};
}
class MockNotificationClickEvent extends MockExtendableEvent {
constructor(readonly notification: Object, readonly action?: string) { super(); }
}
class MockInstallEvent extends MockExtendableEvent {}