Skip to content

Commit a21ff80

Browse files
committed
Add IT for service worker
Fixes #2867
1 parent 2654b66 commit a21ff80

File tree

5 files changed

+233
-25
lines changed

5 files changed

+233
-25
lines changed

packages/java/tests/spring/runtime/frontend/index.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@ import './test-view.js';
22
import './about-view.js';
33
import { Route, Router } from '@vaadin/router';
44

5-
const routes: Route[] = [{ path: '/', component: 'test-view' },
6-
{ path: '/about-view', component: 'about-view' }];
5+
const routes: Route[] = [
6+
{ path: '/', component: 'test-view' },
7+
{ path: '/about-view', component: 'about-view' },
8+
];
79

810
// Vaadin router needs an outlet in the index.html page to display views
911
const router = new Router(document.querySelector('#outlet'));
1012
router.setRoutes(routes);
13+
14+
if ('serviceWorker' in navigator) {
15+
navigator.serviceWorker.ready.then((registration) => {
16+
if (!registration.active) {
17+
return;
18+
}
19+
20+
registration.active.postMessage({ message: 'Hello' });
21+
});
22+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference lib="webworker" />
2+
3+
addEventListener('message', (event) => {
4+
if (event.data.message === 'Hello') {
5+
event.source?.postMessage({ message: 'Hi' });
6+
}
7+
});
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/// <reference lib="webworker" />
2+
3+
//Copy from generated target/sw.ts with extra import ./sw-extended.js
4+
5+
importScripts('sw-runtime-resources-precache.js');
6+
import './sw-extended.js';
7+
import { clientsClaim, cacheNames, WorkboxPlugin } from 'workbox-core';
8+
import { matchPrecache, precacheAndRoute, getCacheKeyForURL } from 'workbox-precaching';
9+
import { NavigationRoute, registerRoute } from 'workbox-routing';
10+
import { PrecacheEntry } from 'workbox-precaching/_types';
11+
import { NetworkOnly, NetworkFirst } from 'workbox-strategies';
12+
13+
declare var self: ServiceWorkerGlobalScope & {
14+
__WB_MANIFEST: Array<PrecacheEntry>;
15+
additionalManifestEntries?: Array<PrecacheEntry>;
16+
};
17+
18+
self.skipWaiting();
19+
clientsClaim();
20+
21+
declare var OFFLINE_PATH: string; // defined by Webpack/Vite
22+
declare var VITE_ENABLED: boolean; // defined by Webpack/Vite
23+
24+
// Combine manifest entries injected at compile-time by Webpack/Vite
25+
// with ones that Flow injects at runtime through `sw-runtime-resources-precache.js`.
26+
let manifestEntries: PrecacheEntry[] = self.__WB_MANIFEST || [];
27+
// If injected entries contains element for root, then discard the one from Flow
28+
// may only happen when running in development mode, but after a frontend build
29+
let hasRootEntry = manifestEntries.findIndex((entry) => entry.url === '.') >= 0;
30+
if (self.additionalManifestEntries?.length) {
31+
manifestEntries.push(...self.additionalManifestEntries.filter( (entry) => entry.url !== '.' || !hasRootEntry));
32+
}
33+
34+
const offlinePath = OFFLINE_PATH;
35+
36+
// Compute the registration scope path.
37+
// Example: http://localhost:8888/scope-path/sw.js => /scope-path/
38+
const scope = new URL(self.registration.scope);
39+
40+
/**
41+
* Replaces <base href> in pre-cached response HTML with the service worker’s
42+
* scope URL.
43+
*
44+
* @param response HTML response to modify
45+
* @returns modified response
46+
*/
47+
async function rewriteBaseHref(response: Response) {
48+
const html = await response.text();
49+
return new Response(html.replace(/<base\s+href=[^>]*>/, `<base href="${self.registration.scope}">`), response);
50+
};
51+
52+
/**
53+
* Returns true if the given URL is included in the manifest, otherwise false.
54+
*/
55+
function isManifestEntryURL(url: URL) {
56+
return manifestEntries.some((entry) => getCacheKeyForURL(entry.url) === getCacheKeyForURL(`${url}`));
57+
}
58+
59+
/**
60+
* A workbox plugin that checks and updates the network connection status
61+
* on every fetch request.
62+
*/
63+
let connectionLost = false;
64+
function checkConnectionPlugin(): WorkboxPlugin {
65+
return {
66+
async fetchDidFail() {
67+
connectionLost = true;
68+
},
69+
async fetchDidSucceed({ response }) {
70+
connectionLost = false;
71+
return response
72+
}
73+
}
74+
}
75+
76+
const networkOnly = new NetworkOnly({
77+
plugins: [checkConnectionPlugin()]
78+
});
79+
const networkFirst = new NetworkFirst({
80+
plugins: [checkConnectionPlugin()]
81+
});
82+
83+
if (process.env.NODE_ENV === 'development' && VITE_ENABLED) {
84+
self.addEventListener('activate', (event) => {
85+
event.waitUntil(caches.delete(cacheNames.runtime));
86+
});
87+
88+
// Cache /VAADIN/* resources in dev mode. Ensure the Vite specific URLs on another port are not handled to avoid excessive logging.
89+
registerRoute(
90+
({ url }) => url.port === scope.port && url.pathname.startsWith(`${scope.pathname}VAADIN/`),
91+
networkFirst
92+
);
93+
}
94+
95+
registerRoute(
96+
new NavigationRoute(async (context) => {
97+
async function serveOfflineFallback() {
98+
const response = await matchPrecache(offlinePath);
99+
return response ? rewriteBaseHref(response) : undefined;
100+
}
101+
102+
function serveResourceFromCache() {
103+
// Always serve the offline fallback at the scope path.
104+
if (context.url.pathname === scope.pathname) {
105+
return serveOfflineFallback();
106+
}
107+
108+
if (isManifestEntryURL(context.url)) {
109+
return matchPrecache(context.request);
110+
}
111+
112+
return serveOfflineFallback();
113+
};
114+
115+
// Try to serve the resource from the cache when offline is detected.
116+
if (!self.navigator.onLine) {
117+
const response = await serveResourceFromCache();
118+
if (response) {
119+
return response;
120+
}
121+
}
122+
123+
// Sometimes navigator.onLine is not reliable,
124+
// try to serve the resource from the cache also in the case of a network failure.
125+
try {
126+
return await networkOnly.handle(context);
127+
} catch (error) {
128+
const response = await serveResourceFromCache();
129+
if (response) {
130+
return response;
131+
}
132+
throw error;
133+
}
134+
})
135+
);
136+
137+
precacheAndRoute(manifestEntries);
138+
139+
self.addEventListener('message', (event) => {
140+
if (typeof event.data !== 'object' || !('method' in event.data)) {
141+
return;
142+
}
143+
144+
// JSON-RPC request handler for ConnectionStateStore
145+
if (event.data.method === 'Vaadin.ServiceWorker.isConnectionLost' && 'id' in event.data) {
146+
event.source?.postMessage({ id: event.data.id, result: connectionLost }, []);
147+
}
148+
});
149+
150+
// Handle web push
151+
152+
self.addEventListener('push', (e) => {
153+
const data = e.data?.json();
154+
if (data) {
155+
self.registration.showNotification(data.title, {
156+
body: data.body,
157+
});
158+
}
159+
});
160+
161+
self.addEventListener('notificationclick', (e) => {
162+
e.notification.close();
163+
e.waitUntil(focusOrOpenWindow());
164+
});
165+
166+
async function focusOrOpenWindow() {
167+
const url = new URL('/', self.location.origin).href;
168+
169+
const allWindows = await self.clients.matchAll({
170+
type: 'window',
171+
});
172+
const appWindow = allWindows.find((w) => w.url === url);
173+
174+
if (appWindow) {
175+
return appWindow.focus();
176+
} else {
177+
return self.clients.openWindow(url);
178+
}
179+
}
Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
import { LitElement, html } from 'lit';
2-
import { customElement } from 'lit/decorators.js';
2+
import { customElement, query } from 'lit/decorators.js';
33

44
@customElement('test-view')
55
export class TestView extends LitElement {
6+
@query('#sw-content')
7+
private swContent!: HTMLElement;
8+
9+
connectedCallback() {
10+
super.connectedCallback();
11+
if ('serviceWorker' in navigator) {
12+
navigator.serviceWorker.addEventListener('message', (event) => {
13+
if (event.data.message !== 'Hi') {
14+
return;
15+
}
16+
this.swContent.textContent = 'Hey from SW';
17+
});
18+
}
19+
}
20+
621
render() {
7-
return html` <div>Hello</div> `;
22+
return html`
23+
<div>
24+
<div id="sw-content"></div>
25+
<div>Test</div>
26+
</div>
27+
`;
828
}
929
}

packages/java/tests/spring/runtime/src/test/java/com/vaadin/flow/connect/ServiceWorkerIT.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package com.vaadin.flow.connect;
1717

1818
import com.vaadin.flow.testutil.ChromeDeviceTest;
19+
import com.vaadin.testbench.TestBenchElement;
1920
import org.junit.Assert;
2021
import org.junit.Test;
2122
import org.openqa.selenium.By;
2223
import org.openqa.selenium.JavascriptExecutor;
24+
import org.openqa.selenium.support.ui.ExpectedConditions;
2325

2426
public class ServiceWorkerIT extends ChromeDeviceTest {
2527

@@ -29,40 +31,28 @@ public void onlineRoot_serviceWorkerInstalled_serviceWorkerActive() {
2931
waitForDevServer();
3032
waitForServiceWorkerReady();
3133

32-
boolean serviceWorkerActive = (boolean) ((JavascriptExecutor) getDriver())
33-
.executeAsyncScript(
34-
"const resolve = arguments[arguments.length - 1];"
35-
+ "navigator.serviceWorker.ready.then( function(reg) { resolve(!!reg.active); });");
34+
boolean serviceWorkerActive = (boolean) ((JavascriptExecutor) getDriver()).executeAsyncScript("const resolve = arguments[arguments.length - 1];" + "navigator.serviceWorker.ready.then( function(reg) { resolve(!!reg.active); });");
3635
Assert.assertTrue("service worker not installed", serviceWorkerActive);
3736
}
3837

3938
@Test
40-
public void offlineRoot_reload_viewReloaded() {
39+
public void onlineRoot_serviceWorkerInstalled_serviceWorkerResponsive() {
4140
openPageAndPreCacheWhenDevelopmentMode("/");
41+
Assert.assertNotNull("Should have outlet when loaded online", findElement(By.id("outlet")));
42+
Assert.assertNotNull("Should have <test-view> in DOM when loaded online", findElement(By.tagName("test-view")));
43+
TestBenchElement testView = $("test-view").waitForFirst();
4244

43-
// Confirm that app shell is loaded
44-
Assert.assertNotNull("Should have outlet when loaded online",
45-
findElement(By.id("outlet")));
46-
47-
// Confirm that client side view is loaded
48-
Assert.assertNotNull(
49-
"Should have <test-view> in DOM when loaded online",
50-
findElement(By.tagName("test-view")));
51-
52-
// Reload the page in offline mode
53-
executeScript("window.location.reload();");
54-
waitUntil(webDriver -> ((JavascriptExecutor) driver)
55-
.executeScript("return document.readyState")
56-
.equals("complete"));
45+
waitUntil(ExpectedConditions.textToBePresentInElement(
46+
testView.$(TestBenchElement.class).id("sw-content"),
47+
"Hey from SW"), 25);
5748
}
5849

5950
private void openPageAndPreCacheWhenDevelopmentMode(String targetView) {
6051
openPageAndPreCacheWhenDevelopmentMode(targetView, () -> {
6152
});
6253
}
6354

64-
private void openPageAndPreCacheWhenDevelopmentMode(String targetView,
65-
Runnable activateViews) {
55+
private void openPageAndPreCacheWhenDevelopmentMode(String targetView, Runnable activateViews) {
6656
getDriver().get(getRootURL() + targetView);
6757
waitForDevServer();
6858
waitForServiceWorkerReady();

0 commit comments

Comments
 (0)