feat(service-worker): add `openWindow`, `focusLastFocusedOrOpen` and `navigateLastFocusedOrOpen` (#42520)
Add `openWindow`, `focusLastFocusedOrOpen` and `navigateLastFocusedOrOpen` abilty to the notificationclick handler such that when either a notification or notification action is clicked the service-worker can act accordinly without the need for the app to be open PR Close #26907 PR Close #42520
This commit is contained in:
parent
9de65dbdce
commit
d546501ab5
|
@ -617,6 +617,7 @@ groups:
|
|||
'aio/content/guide/service-worker-config.md',
|
||||
'aio/content/guide/service-worker-devops.md',
|
||||
'aio/content/guide/service-worker-intro.md',
|
||||
'aio/content/guide/service-worker-notifications.md',
|
||||
'aio/content/images/guide/service-worker/**'
|
||||
])
|
||||
reviewers:
|
||||
|
|
|
@ -95,4 +95,4 @@ You can subscribe to `SwUpdate#unrecoverable` to be notified and handle these er
|
|||
## More on Angular service workers
|
||||
|
||||
You may also be interested in the following:
|
||||
* [Service Worker in Production](guide/service-worker-devops).
|
||||
* [Service Worker Notifications](guide/service-worker-notifications).
|
|
@ -66,6 +66,7 @@ The rest of the articles in this section specifically address the Angular implem
|
|||
|
||||
* [App Shell](guide/app-shell)
|
||||
* [Service Worker Communication](guide/service-worker-communications)
|
||||
* [Service Worker Notifications](guide/service-worker-notifications)
|
||||
* [Service Worker in Production](guide/service-worker-devops)
|
||||
* [Service Worker Configuration](guide/service-worker-config)
|
||||
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# Service worker notifications
|
||||
|
||||
Push notifications are a compelling way to engage users. Through the power of service workers, notifications can be delivered to a device even when your application is not in focus.
|
||||
|
||||
The Angular service worker enables the display of push notifications and the handling of notification click events.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
When using the Angular service worker, push notification interactions are handled using the `SwPush` service.
|
||||
To learn more about the native APIs involved see [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) and [Using the Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API).
|
||||
|
||||
</div>
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
We recommend you have a basic understanding of the following:
|
||||
|
||||
- [Getting Started with Service Workers](guide/service-worker-getting-started).
|
||||
|
||||
## Notification payload
|
||||
|
||||
Invoke push notifications by pushing a message with a valid payload. See `SwPush` for guidance.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
In Chrome, you can test push notifications without a backend.
|
||||
Open Devtools -> Application -> Service Workers and use the `Push` input to send a JSON notification payload.
|
||||
|
||||
</div>
|
||||
|
||||
## Notification click handling
|
||||
|
||||
The default behavior for the `notificationclick` event is to close the notification and notify `SwPush.notificationClicks`.
|
||||
|
||||
You can specify an additional operation to be executed on `notificationclick` by adding an `onActionClick` property to the `data` object, and providing a `default` entry. This is especially useful for when there are no open clients when a notification is clicked.
|
||||
|
||||
```json
|
||||
{
|
||||
"notification": {
|
||||
"title": "New Notification!",
|
||||
"data": {
|
||||
"onActionClick": {
|
||||
"default": {"operation": "openWindow", "url": "foo"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Operations
|
||||
|
||||
The Angular service worker supports the following operations:
|
||||
|
||||
- `openWindow`: Opens a new tab at the specified URL, which is resolved relative to the service worker scope.
|
||||
|
||||
- `focusLastFocusedOrOpen`: Focuses the last focused client. If there is no client open, then it opens a new tab at the specified URL, which is resolved relative to the service worker scope.
|
||||
|
||||
- `navigateLastFocusedOrOpen`: Focuses the last focused client and navigates it to the specified URL, which is resolved relative to the service worker scope. If there is no client open, then it opens a new tab at the specified URL.
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
If an `onActionClick` item does not define a `url`, then the service worker's registration scope is used.
|
||||
|
||||
</div>
|
||||
|
||||
### Actions
|
||||
|
||||
Actions offer a way to customize how the user can interact with a notification.
|
||||
|
||||
Using the `actions` property, you can define a set of available actions. Each action is represented as an action button that the user can click to interact with the notification.
|
||||
|
||||
In addition, using the `onActionClick` property on the `data` object, you can tie each action to an operation to be performed when the corresponding action button is clicked:
|
||||
|
||||
```ts
|
||||
{
|
||||
"notification": {
|
||||
"title": "New Notification!",
|
||||
"actions": [
|
||||
{"action": "foo", "title": "Open new tab"},
|
||||
{"action": "bar", "title": "Focus last"},
|
||||
{"action": "baz", "title": "Navigate last"},
|
||||
{"action": "qux", "title": "Just notify existing clients"}
|
||||
],
|
||||
"data": {
|
||||
"onActionClick": {
|
||||
"default": {"operation": "openWindow"},
|
||||
"foo": {"operation": "openWindow", "url": "/absolute/path"},
|
||||
"bar": {"operation": "focusLastFocusedOrOpen", "url": "relative/path"},
|
||||
"baz": {"operation": "navigateLastFocusedOrOpen", "url": "https://other.domain.com/"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
If an action does not have a corresponding `onActionClick` entry, then the notification is closed and `SwPush.notificationClicks` is notified on existing clients.
|
||||
|
||||
</div>
|
||||
|
||||
## More on Angular service workers
|
||||
|
||||
You may also be interested in the following:
|
||||
|
||||
- [Service Worker in Production](guide/service-worker-devops).
|
|
@ -435,6 +435,11 @@
|
|||
"title": "Service Worker Communication",
|
||||
"tooltip": "Services that enable you to interact with an Angular service worker."
|
||||
},
|
||||
{
|
||||
"url": "guide/service-worker-notifications",
|
||||
"title": "Service Worker Notifications",
|
||||
"tooltip": "Configuring service worker notification behavior."
|
||||
},
|
||||
{
|
||||
"url": "guide/service-worker-devops",
|
||||
"title": "Service Worker in Production",
|
||||
|
|
|
@ -81,6 +81,9 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';
|
|||
* <code-example path="service-worker/push/module.ts" region="subscribe-to-notification-clicks"
|
||||
* header="app.component.ts"></code-example>
|
||||
*
|
||||
* You can read more on handling notification clicks in the [Service worker notifications
|
||||
* guide](guide/service-worker-notifications).
|
||||
*
|
||||
* @see [Push Notifications](https://developers.google.com/web/fundamentals/codelabs/push-notifications/)
|
||||
* @see [Angular Push Notifications](https://blog.angular-university.io/angular-push-notifications/)
|
||||
* @see [MDN: Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
|
||||
|
|
|
@ -348,12 +348,52 @@ export class Driver implements Debuggable, UpdateSource {
|
|||
NOTIFICATION_OPTION_NAMES.filter(name => name in notification)
|
||||
.forEach(name => options[name] = notification[name]);
|
||||
|
||||
const notificationAction = action === '' || action === undefined ? 'default' : action;
|
||||
|
||||
const onActionClick = notification?.data?.onActionClick[notificationAction];
|
||||
|
||||
const urlToOpen = new URL(onActionClick?.url ?? '', this.scope.registration.scope).href;
|
||||
|
||||
switch (onActionClick?.operation) {
|
||||
case 'openWindow':
|
||||
await this.scope.clients.openWindow(urlToOpen);
|
||||
break;
|
||||
case 'focusLastFocusedOrOpen': {
|
||||
let matchingClient = await this.getLastFocusedMatchingClient(this.scope);
|
||||
if (matchingClient) {
|
||||
await matchingClient?.focus();
|
||||
} else {
|
||||
await this.scope.clients.openWindow(urlToOpen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'navigateLastFocusedOrOpen': {
|
||||
let matchingClient = await this.getLastFocusedMatchingClient(this.scope);
|
||||
if (matchingClient) {
|
||||
matchingClient = await matchingClient.navigate(urlToOpen);
|
||||
await matchingClient?.focus();
|
||||
} else {
|
||||
await this.scope.clients.openWindow(urlToOpen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
await this.broadcast({
|
||||
type: 'NOTIFICATION_CLICK',
|
||||
data: {action, notification: options},
|
||||
});
|
||||
}
|
||||
|
||||
private async getLastFocusedMatchingClient(scope: ServiceWorkerGlobalScope):
|
||||
Promise<WindowClient|null> {
|
||||
const windowClients = await scope.clients.matchAll({type: 'window'});
|
||||
|
||||
// As per the spec windowClients are `sorted in the most recently focused order`
|
||||
return windowClients[0];
|
||||
}
|
||||
private async reportStatus(client: Client, promise: Promise<void>, nonce: number): Promise<void> {
|
||||
const response = {type: 'STATUS', nonce, status: true};
|
||||
try {
|
||||
|
|
|
@ -36,9 +36,12 @@ declare class Client {
|
|||
}
|
||||
|
||||
interface Clients {
|
||||
claim(): Promise<any>;
|
||||
claim(): Promise<void>;
|
||||
get(id: string): Promise<Client>;
|
||||
matchAll(options?: ClientMatchOptions): Promise<Array<Client>>;
|
||||
matchAll<T extends ClientMatchOptions>(
|
||||
options?: T
|
||||
): Promise<ReadonlyArray<T['type'] extends 'window' ? WindowClient : Client>>;
|
||||
openWindow(url: string): Promise<WindowClient | null>;
|
||||
}
|
||||
|
||||
interface ClientMatchOptions {
|
||||
|
@ -46,11 +49,12 @@ interface ClientMatchOptions {
|
|||
type?: ClientMatchTypes;
|
||||
}
|
||||
|
||||
interface WindowClient {
|
||||
focused: boolean;
|
||||
visibilityState: WindowClientState;
|
||||
interface WindowClient extends Client {
|
||||
readonly ancestorOrigins: ReadonlyArray<string>;
|
||||
readonly focused: boolean;
|
||||
readonly visibilityState: VisibilityState;
|
||||
focus(): Promise<WindowClient>;
|
||||
navigate(url: string): Promise<WindowClient>;
|
||||
navigate(url: string): Promise<WindowClient | null>;
|
||||
}
|
||||
|
||||
type ClientFrameType = 'auxiliary'|'top-level'|'nested'|'none';
|
||||
|
|
|
@ -14,7 +14,7 @@ import {sha1} from '../src/sha1';
|
|||
import {clearAllCaches, MockCache} from '../testing/cache';
|
||||
import {MockRequest, MockResponse} from '../testing/fetch';
|
||||
import {MockFileSystem, MockFileSystemBuilder, MockServerState, MockServerStateBuilder, tmpHashTableForFs} from '../testing/mock';
|
||||
import {SwTestHarness, SwTestHarnessBuilder} from '../testing/scope';
|
||||
import {MockClient, SwTestHarness, SwTestHarnessBuilder, WindowClientImpl} from '../testing/scope';
|
||||
|
||||
(function() {
|
||||
// Skip environments that don't support the minimum APIs needed to run the SW tests.
|
||||
|
@ -721,30 +721,364 @@ describe('Driver', () => {
|
|||
}]);
|
||||
});
|
||||
|
||||
it('broadcasts notification click events with action', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{title: 'This is a test with action', body: 'Test body with action'}, 'button');
|
||||
const message: any = scope.clients.getMock('default')!.messages[0];
|
||||
describe('notification click events', () => {
|
||||
it('broadcasts notification click events with action', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{title: 'This is a test with action', body: 'Test body with action'}, 'button');
|
||||
const message: any = scope.clients.getMock('default')!.messages[0];
|
||||
|
||||
expect(message.type).toEqual('NOTIFICATION_CLICK');
|
||||
expect(message.data.action).toEqual('button');
|
||||
expect(message.data.notification.title).toEqual('This is a test with action');
|
||||
expect(message.data.notification.body).toEqual('Test body with action');
|
||||
});
|
||||
expect(message.type).toEqual('NOTIFICATION_CLICK');
|
||||
expect(message.data.action).toEqual('button');
|
||||
expect(message.data.notification.title).toEqual('This is a test with action');
|
||||
expect(message.data.notification.body).toEqual('Test body with action');
|
||||
});
|
||||
|
||||
it('broadcasts notification click events without action', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{title: 'This is a test without action', body: 'Test body without action'});
|
||||
const message: any = scope.clients.getMock('default')!.messages[0];
|
||||
it('broadcasts notification click events without action', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
await driver.initialized;
|
||||
await scope.handleClick({
|
||||
title: 'This is a test without action',
|
||||
body: 'Test body without action',
|
||||
});
|
||||
const message: any = scope.clients.getMock('default')!.messages[0];
|
||||
|
||||
expect(message.type).toEqual('NOTIFICATION_CLICK');
|
||||
expect(message.data.action).toBeUndefined();
|
||||
expect(message.data.notification.title).toEqual('This is a test without action');
|
||||
expect(message.data.notification.body).toEqual('Test body without action');
|
||||
expect(message.type).toEqual('NOTIFICATION_CLICK');
|
||||
expect(message.data.action).toBeUndefined();
|
||||
expect(message.data.notification.title).toEqual('This is a test without action');
|
||||
expect(message.data.notification.body).toEqual('Test body without action');
|
||||
});
|
||||
|
||||
describe('Client interactions', () => {
|
||||
describe('`openWindow` operation', () => {
|
||||
it('opens a new client window at url', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
const url = 'foo';
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with url',
|
||||
body: 'Test body with url',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'openWindow', url},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow)
|
||||
.toHaveBeenCalledWith(`${scope.registration.scope}${url}`);
|
||||
});
|
||||
|
||||
it('opens a new client window with `/` when no `url`', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test without url',
|
||||
body: 'Test body without url',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'openWindow'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).toHaveBeenCalledWith(`${scope.registration.scope}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`focusLastFocusedOrOpen` operation', () => {
|
||||
it('focuses last client keeping previous url', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
const mockClient = new WindowClientImpl('fooBar');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([mockClient]));
|
||||
spyOn(mockClient, 'focus');
|
||||
spyOn(mockClient, 'navigate');
|
||||
const url = 'foo';
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation focusLastFocusedOrOpen',
|
||||
body: 'Test body with operation focusLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'focusLastFocusedOrOpen', url},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(mockClient.navigate).not.toHaveBeenCalled();
|
||||
expect(mockClient.url).toEqual('http://localhost/unique');
|
||||
expect(mockClient.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to openWindow at url when no last client to focus', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([]));
|
||||
const url = 'foo';
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation focusLastFocusedOrOpen',
|
||||
body: 'Test body with operation focusLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'focusLastFocusedOrOpen', url},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow)
|
||||
.toHaveBeenCalledWith(`${scope.registration.scope}${url}`);
|
||||
});
|
||||
|
||||
it('falls back to openWindow at `/` when no last client and no `url`', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([]));
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation focusLastFocusedOrOpen',
|
||||
body: 'Test body with operation focusLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'focusLastFocusedOrOpen'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).toHaveBeenCalledWith(`${scope.registration.scope}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`navigateLastFocusedOrOpen` operation', () => {
|
||||
it('navigates last client to `url`', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
const mockClient = new WindowClientImpl('fooBar');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([mockClient]));
|
||||
spyOn(mockClient, 'focus');
|
||||
spyOn(mockClient, 'navigate').and.returnValue(Promise.resolve(mockClient));
|
||||
const url = 'foo';
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation navigateLastFocusedOrOpen',
|
||||
body: 'Test body with operation navigateLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'navigateLastFocusedOrOpen', url},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(mockClient.navigate).toHaveBeenCalledWith(`${scope.registration.scope}${url}`);
|
||||
expect(mockClient.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('navigates last client to `/` if no `url', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
const mockClient = new WindowClientImpl('fooBar');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([mockClient]));
|
||||
spyOn(mockClient, 'focus');
|
||||
spyOn(mockClient, 'navigate').and.returnValue(Promise.resolve(mockClient));
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation navigateLastFocusedOrOpen',
|
||||
body: 'Test body with operation navigateLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'navigateLastFocusedOrOpen'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(mockClient.navigate).toHaveBeenCalledWith(`${scope.registration.scope}`);
|
||||
expect(mockClient.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to openWindow at url when no last client to focus', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([]));
|
||||
const url = 'foo';
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation navigateLastFocusedOrOpen',
|
||||
body: 'Test body with operation navigateLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'navigateLastFocusedOrOpen', url},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow)
|
||||
.toHaveBeenCalledWith(`${scope.registration.scope}${url}`);
|
||||
});
|
||||
|
||||
it('falls back to openWindow at `/` when no last client and no `url`', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
spyOn(scope.clients, 'matchAll').and.returnValue(Promise.resolve([]));
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with operation navigateLastFocusedOrOpen',
|
||||
body: 'Test body with operation navigateLastFocusedOrOpen',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'navigateLastFocusedOrOpen'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).toHaveBeenCalledWith(`${scope.registration.scope}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('No matching onActionClick field', () => {
|
||||
it('no client interaction', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test without onActionClick field',
|
||||
body: 'Test body without onActionClick field',
|
||||
data: {
|
||||
onActionClick: {
|
||||
fooz: {operation: 'focusLastFocusedOrOpen', url: 'fooz'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('no action', () => {
|
||||
it('uses onActionClick default when no specific action is clicked', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
const url = 'fooz';
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test without action',
|
||||
body: 'Test body without action',
|
||||
data: {
|
||||
onActionClick: {
|
||||
default: {operation: 'openWindow', url},
|
||||
},
|
||||
},
|
||||
},
|
||||
'');
|
||||
expect(scope.clients.openWindow)
|
||||
.toHaveBeenCalledWith(`${scope.registration.scope}${url}`);
|
||||
});
|
||||
|
||||
describe('no onActionClick default', () => {
|
||||
it('has no client interaction', async () => {
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{title: 'This is a test without action', body: 'Test body without action'});
|
||||
expect(scope.clients.openWindow).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL resolution', () => {
|
||||
it('should resolve relative to service worker scope', async () => {
|
||||
(scope.registration.scope as string) = 'http://localhost/foo/bar/';
|
||||
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with a relative url',
|
||||
body: 'Test body with a relative url',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'openWindow', url: 'baz/qux'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).toHaveBeenCalledWith('http://localhost/foo/bar/baz/qux');
|
||||
});
|
||||
|
||||
it('should resolve with an absolute path', async () => {
|
||||
(scope.registration.scope as string) = 'http://localhost/foo/bar/';
|
||||
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with an absolute path url',
|
||||
body: 'Test body with an absolute path url',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'openWindow', url: '/baz/qux'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).toHaveBeenCalledWith('http://localhost/baz/qux');
|
||||
});
|
||||
|
||||
it('should resolve other origins', async () => {
|
||||
(scope.registration.scope as string) = 'http://localhost/foo/bar/';
|
||||
|
||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||
|
||||
spyOn(scope.clients, 'openWindow');
|
||||
|
||||
await driver.initialized;
|
||||
await scope.handleClick(
|
||||
{
|
||||
title: 'This is a test with external origin',
|
||||
body: 'Test body with external origin',
|
||||
data: {
|
||||
onActionClick: {
|
||||
foo: {operation: 'openWindow', url: 'http://other.host/baz/qux'},
|
||||
},
|
||||
},
|
||||
},
|
||||
'foo');
|
||||
expect(scope.clients.openWindow).toHaveBeenCalledWith('http://other.host/baz/qux');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('prefetches updates to lazy cache when set', async () => {
|
||||
|
|
|
@ -31,6 +31,24 @@ export class MockClient {
|
|||
this.queue.next(message);
|
||||
}
|
||||
}
|
||||
export class WindowClientImpl extends MockClient implements WindowClient {
|
||||
readonly ancestorOrigins: ReadonlyArray<string> = [];
|
||||
readonly focused: boolean = false;
|
||||
readonly visibilityState: VisibilityState = 'hidden';
|
||||
frameType: ClientFrameType = 'top-level';
|
||||
url = 'http://localhost/unique';
|
||||
|
||||
constructor(readonly id: string) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
async focus(): Promise<WindowClient> {
|
||||
return this;
|
||||
}
|
||||
async navigate(url: string): Promise<WindowClient|null> {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class SwTestHarnessBuilder {
|
||||
private origin = parseUrl(this.scopeUrl).origin;
|
||||
|
@ -76,8 +94,12 @@ export class MockClients implements Clients {
|
|||
return this.clients.get(id);
|
||||
}
|
||||
|
||||
async matchAll(): Promise<Client[]> {
|
||||
return Array.from(this.clients.values()) as any[] as Client[];
|
||||
async matchAll<T extends ClientQueryOptions>(options?: T):
|
||||
Promise<ReadonlyArray<T['type'] extends 'window'? WindowClient : Client>> {
|
||||
return Array.from(this.clients.values()) as any[];
|
||||
}
|
||||
async openWindow(url: string): Promise<WindowClient|null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async claim(): Promise<any> {}
|
||||
|
|
Loading…
Reference in New Issue