Skip to content

Commit

Permalink
[WPT] BFCache: service worker clients
Browse files Browse the repository at this point in the history
This CL adds service worker tests for BFCache:

- navigator.serviceWorker.controller
- Fetch interception
- Clients.claim()
- Clients.matchAll() and
- unregister().

Expected behavior:

- Controlled pages should remain controlled after
  restored from BFCache, i.e.
  navigator.serviceWorker.controller should remain non-null
  and fetch should be intercepted.
- Clients.claim() should evict pages
  that would be affected from BFCache.
- Clients.matchAll() shouldn't list pages in BFCache.
- unregister() should evict controlled pages from BFCache.

Failing tests:

- service-worker-clients-claim.https.html:
  On Safari/Firefox, Clients.claim() doesn't evict pages from BFCache.
- service-worker-controlled-after-restore.https.html:
  On Firefox, fetches are not intercepted after restored from
  BFCache while `navigator.serviceWorker.controller` is non-null.
- service-worker-unregister.https.html:
  Controlled pages aren't evicted by unregister() (all browsers).

Bug: 1107415, 1204228, w3c/ServiceWorker#1594
Change-Id: I73233cf917e31dd91b974823d5490d0190f0eade
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3201011
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Ben Kelly <wanderview@chromium.org>
Commit-Queue: Hiroshige Hayashizaki <hiroshige@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1008327}
  • Loading branch information
hiroshige-g authored and chromium-wpt-export-bot committed May 27, 2022
1 parent ba1428c commit e08e12d
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,16 @@ function runBfcacheTest(params, description) {
}
}, description);
}

// Call clients.claim() on the service worker
async function claim(t, worker) {
const channel = new MessageChannel();
const saw_message = new Promise(function(resolve) {
channel.port1.onmessage = t.step_func(function(e) {
assert_equals(e.data, 'PASS', 'Worker call to claim() should fulfill.');
resolve();
});
});
worker.postMessage({port: channel.port2}, [channel.port2]);
await saw_message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
self.addEventListener('message', function(event) {
self.clients.claim()
.then(function(result) {
if (result !== undefined) {
event.data.port.postMessage(
'FAIL: claim() should be resolved with undefined');
return;
}
event.data.port.postMessage('PASS');
})
.catch(function(error) {
event.data.port.postMessage('FAIL: exception: ' + error.name);
});
});

self.addEventListener('fetch', e => {
if (e.request.url.match(/\/is-controlled/)) {
e.respondWith(new Response('controlled'));
}
else if (e.request.url.match(/\/get-clients-matchall/)) {
const options = { includeUncontrolled: true, type: 'all' };
e.respondWith(
self.clients.matchAll(options)
.then(clients => {
const client_urls = [];
clients.forEach(client => client_urls.push(client.url));
return new Response(JSON.stringify(client_urls));
})
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="resources/helper.sub.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<script>
// Calling Clients.claim() on the service worker when a controlled page is in
// BFCache should evict the page from BFCache, as per
// https://github.com/w3c/ServiceWorker/issues/1038#issuecomment-291028845.
promise_test(async t => {
const pageA = new RemoteContext(token());
const pageB = new RemoteContext(token());

const urlA = location.origin + executorPath + pageA.context_id;
const urlB = originCrossSite + executorPath + pageB.context_id;

window.open(urlA, '_blank', 'noopener');
await pageA.execute_script(waitForPageShow);

// Register a service worker after `pageA` is loaded to make `pageA`
// uncontrolled at this time.
const workerUrl =
'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
const registration =
await service_worker_unregister_and_register(t, workerUrl, './');
t.add_cleanup(_ => registration.unregister());
await wait_for_state(t, registration.installing, 'activated');

// Navigate to `urlB`.
await pageA.execute_script(
(url) => {
prepareNavigation(() => { location.href = url; });
},
[urlB]);
await pageB.execute_script(waitForPageShow);

// Call Clients.claim() on the service worker when `pageA` is in BFCache.
const controllerChanged = new Promise(
resolve => navigator.serviceWorker.oncontrollerchange = resolve);
await claim(t, registration.active);
await controllerChanged;

// `pageA` doesn't appear in matchAll().
const clients1 = await (await fetch('/get-clients-matchall')).json();
assert_true(clients1.indexOf(urlA) < 0,
'1: matchAll() before back navigation');

// Back navigate and check that the page was evicted from BFCache.
await pageB.execute_script(
() => {
prepareNavigation(() => { history.back(); });
}
);
await pageA.execute_script(waitForPageShow);
await assert_not_bfcached(pageA);

// After back navigation, `pageA` appear in matchAll(), because it was newly
// loaded and controlled by the service worker.
const clients2 = await (await fetch('/get-clients-matchall')).json();
const controlled2 = await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null));
assert_true(clients2.indexOf(urlA) >= 0,
'2: matchAll() just after back navigation');
assert_true(controlled2,
'2: pageA should be controlled just after back navigation');

}, 'Clients.claim() evicts pages that would be affected from BFCache');
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!doctype html>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="resources/helper.sub.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<script>
promise_test(async t => {
// Register a service worker and make this page controlled.
const workerUrl =
'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
const registration =
await service_worker_unregister_and_register(t, workerUrl, './');
t.add_cleanup(_ => registration.unregister());
await wait_for_state(t, registration.installing, 'activated');
const controllerChanged = new Promise(
resolve => navigator.serviceWorker.oncontrollerchange = resolve);
await claim(t, registration.active);
await controllerChanged;

const pageA = new RemoteContext(token());
const pageB = new RemoteContext(token());

const urlA = location.origin + executorPath + pageA.context_id;
const urlB = originCrossSite + executorPath + pageB.context_id;

// Open `urlA`.
window.open(urlA, '_blank', 'noopener');
await pageA.execute_script(waitForPageShow);

// Get Clients.matchAll() and check whether `pageA` is controlled.
// Actual `assert_*()` is called after `assert_bfcached()` below.
const clients1 = await (await fetch('/get-clients-matchall')).json();
const controlled1 = await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null));

// Navigate to `urlB` and get Clients.matchAll() when `urlA` is in BFCache.
await pageA.execute_script(
(url) => prepareNavigation(() => {
location.href = url;
}),
[urlB]);
await pageB.execute_script(waitForPageShow);
const clients2 = await (await fetch('/get-clients-matchall')).json();

// Back navigate and check whether the page is restored from BFCache.
await pageB.execute_script(
() => {
prepareNavigation(() => { history.back(); });
}
);
await pageA.execute_script(waitForPageShow);
await assert_bfcached(pageA);

// Get Clients.matchAll() and check whether `pageA` is controlled.
const clients3 = await (await fetch('/get-clients-matchall')).json();
const controlled3 = await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null));

// Clients.matchAll() should not list `urlA` when it is in BFCache.
assert_true(clients1.indexOf(urlA) >= 0,
'1: matchAll() before navigation');
assert_true(clients2.indexOf(urlA) < 0,
'2: matchAll() before back navigation');
assert_true(clients3.indexOf(urlA) >= 0,
'3: matchAll() after back navigation');

// `pageA` should be controlled before/after BFCached.
assert_true(controlled1,
'pageA should be controlled before BFCached');
assert_true(controlled3,
'pageA should be controlled after restored');
}, 'Clients.matchAll() should not list pages in BFCache');
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!doctype html>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="resources/helper.sub.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<script>
promise_test(async t => {
const pageA = new RemoteContext(token());
const pageB = new RemoteContext(token());

const urlA = location.origin + executorPath + pageA.context_id;
const urlB = originCrossSite + executorPath + pageB.context_id;

// Register a service worker.
const workerUrl =
'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
const registration =
await service_worker_unregister_and_register(t, workerUrl, './');
t.add_cleanup(_ => registration.unregister());
await wait_for_state(t, registration.installing, 'activated');

window.open(urlA, '_blank', 'noopener');
await pageA.execute_script(waitForPageShow);

assert_true(
await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null)),
'pageA should be controlled before navigation');

navigateAndThenBack(pageA, pageB, urlB);
await assert_bfcached(pageA);

assert_true(
await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null)),
'navigator.serviceWorker.controller should be non-null ' +
'after restored from BFCache');

const isControlled = await pageA.execute_script(
() => fetch('/is-controlled').then(r => r.text()));

assert_true(
await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null)),
'navigator.serviceWorker.controller should be non-null ' +
'after restored from BFCache and after fetch');

assert_equals(isControlled, 'controlled',
'fetch should be intercepted after restored from BFCache');
}, 'Pages should remain controlled after restored from BFCache');
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!doctype html>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="resources/helper.sub.js"></script>
<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
<script>
// When a service worker is unregistered when a controlled page is in BFCache,
// the page can be still restored from BFCache and remain controlled by the
// service worker.
promise_test(async t => {
// Register a service worker and make this page controlled.
const workerUrl =
'resources/service-worker.js?pipe=header(Service-Worker-Allowed,../)';
const registration =
await service_worker_unregister_and_register(t, workerUrl, './');
t.add_cleanup(_ => registration.unregister());
await wait_for_state(t, registration.installing, 'activated');
const controllerChanged = new Promise(
resolve => navigator.serviceWorker.oncontrollerchange = resolve);
await claim(t, registration.active);
await controllerChanged;

const pageA = new RemoteContext(token());
const pageB = new RemoteContext(token());

const urlA = location.origin + executorPath + pageA.context_id;
const urlB = originCrossSite + executorPath + pageB.context_id;

// Open `urlA`.
window.open(urlA, '_blank', 'noopener');
await pageA.execute_script(waitForPageShow);

assert_true(
await pageA.execute_script(
() => (navigator.serviceWorker.controller !== null)),
'pageA should be controlled before navigation');

// Navigate to `urlB`.
await pageA.execute_script(
(url) => prepareNavigation(() => {
location.href = url;
}),
[urlB]);
await pageB.execute_script(waitForPageShow);

// Unregister the service worker when the controlled `pageA` is in BFCache.
await registration.unregister();

// Back navigate and check whether the page is restored from BFCache.
await pageB.execute_script(
() => {
prepareNavigation(() => { history.back(); });
}
);
await pageA.execute_script(waitForPageShow);
await assert_not_bfcached(pageA);

assert_true(
await pageA.execute_script(
() => (navigator.serviceWorker.controller === null)),
'pageA should not be controlled');

}, 'Unregister service worker while a controlled page is in BFCache');
</script>

0 comments on commit e08e12d

Please sign in to comment.