-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Verify service worker behavior with initial about:blank iframe docume…
…nts. r=JakeA r=mattto (#6304)
- Loading branch information
Ben Kelly
authored
Nov 27, 2017
1 parent
87758be
commit 49e48ef
Showing
6 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
146 changes: 146 additions & 0 deletions
146
service-workers/service-worker/about-blank-replacement.https.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
<!DOCTYPE html> | ||
<title>Service Worker: about:blank replacement handling</title> | ||
<meta name=timeout content=long> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script src="/common/get-host-info.sub.js"></script> | ||
<script src="resources/test-helpers.sub.js"></script> | ||
<body> | ||
<script> | ||
// This test attempts to verify various initial about:blank document | ||
// creation is accurately reflected via the Clients API. The goal is | ||
// for Clients API to reflect what the browser actually does and not | ||
// to make special cases for the API. | ||
// | ||
// If your browser does not create an about:blank document in certain | ||
// cases then please just mark the test expected fail for now. The | ||
// reuse of globals from about:blank documents to the final load document | ||
// has particularly bad interop at the moment. Hopefully we can evolve | ||
// tests like this to eventually align browsers. | ||
|
||
const worker = 'resources/about-blank-replacement-worker.js'; | ||
|
||
// Helper routine that creates an iframe that internally has some kind | ||
// of nested window. The nested window could be another iframe or | ||
// it could be a popup window. | ||
function createFrameWithNestedWindow(url) { | ||
return new Promise((resolve, reject) => { | ||
let frame = document.createElement('iframe'); | ||
frame.src = url; | ||
document.body.appendChild(frame); | ||
|
||
window.addEventListener('message', function onMsg(evt) { | ||
if (evt.data.type !== 'NESTED_LOADED') { | ||
return; | ||
} | ||
window.removeEventListener('message', onMsg); | ||
if (evt.data.result && evt.data.result.startsWith('failure:')) { | ||
reject(evt.data.result); | ||
return; | ||
} | ||
resolve(frame); | ||
}); | ||
}); | ||
} | ||
|
||
// Helper routine to request the given worker find the client with | ||
// the specified URL using the clients.matchAll() API. | ||
function getClientIdByURL(worker, url) { | ||
return new Promise(resolve => { | ||
navigator.serviceWorker.addEventListener('message', function onMsg(evt) { | ||
if (evt.data.type !== 'GET_CLIENT_ID') { | ||
return; | ||
} | ||
navigator.serviceWorker.removeEventListener('message', onMsg); | ||
resolve(evt.data.result); | ||
}); | ||
worker.postMessage({ type: 'GET_CLIENT_ID', url: url.toString() }); | ||
}); | ||
} | ||
|
||
async function doAsyncTest(t, scope, extraSearchParams) { | ||
let reg = await service_worker_unregister_and_register(t, worker, scope); | ||
await wait_for_state(t, reg.installing, 'activated'); | ||
|
||
// Load the scope as a frame. We expect this in turn to have a nested | ||
// iframe. The service worker will intercept the load of the nested | ||
// iframe and populate its body with the client ID of the initial | ||
// about:blank document it sees via clients.matchAll(). | ||
let frame = await createFrameWithNestedWindow(scope); | ||
let initialResult = frame.contentWindow.nested().document.body.textContent; | ||
assert_false(initialResult.startsWith('failure:'), `result: ${initialResult}`); | ||
|
||
// Next, ask the service worker to find the final client ID for the fully | ||
// loaded nested frame. | ||
let nestedURL = new URL(scope, window.location); | ||
nestedURL.searchParams.set('nested', true); | ||
extraSearchParams = extraSearchParams || {}; | ||
for (let p in extraSearchParams) { | ||
nestedURL.searchParams.set(p, extraSearchParams[p]); | ||
} | ||
let finalResult = await getClientIdByURL(reg.active, nestedURL); | ||
assert_false(finalResult.startsWith('failure:'), `result: ${finalResult}`); | ||
|
||
// The initial about:blank client and the final loaded client should have | ||
// the same ID value. | ||
assert_equals(initialResult, finalResult, 'client ID values should match'); | ||
|
||
frame.remove(); | ||
await service_worker_unregister_and_done(t, scope); | ||
} | ||
|
||
promise_test(async function(t) { | ||
// Execute a test where the nested frame is simply loaded normally. | ||
await doAsyncTest(t, 'resources/about-blank-replacement-frame.py'); | ||
}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' + | ||
'matches final Client.'); | ||
|
||
promise_test(async function(t) { | ||
// Execute a test where the nested frame is modified immediately by | ||
// its parent. In this case we add a message listener so the service | ||
// worker can ping the client to verify its existence. This ping-pong | ||
// check is performed during the initial load and when verifying the | ||
// final loaded client. | ||
await doAsyncTest(t, 'resources/about-blank-replacement-ping-frame.py', | ||
{ 'ping': true }); | ||
}, 'Initial about:blank modified by parent is controlled, exposed to ' + | ||
'clients.matchAll(), and matches final Client.'); | ||
|
||
promise_test(async function(t) { | ||
// Execute a test where the nested window is a popup window instead of | ||
// an iframe. This should behave the same as the simple iframe case. | ||
await doAsyncTest(t, 'resources/about-blank-replacement-popup-frame.py'); | ||
}, 'Popup initial about:blank is controlled, exposed to clients.matchAll(), and ' + | ||
'matches final Client.'); | ||
|
||
promise_test(async function(t) { | ||
const scope = 'resources/about-blank-replacement-uncontrolled-nested-frame.html'; | ||
|
||
let reg = await service_worker_unregister_and_register(t, worker, scope); | ||
await wait_for_state(t, reg.installing, 'activated'); | ||
|
||
// Load the scope as a frame. We expect this in turn to have a nested | ||
// iframe. Unlike the other tests in this file the nested iframe URL | ||
// is not covered by a service worker scope. It should end up as | ||
// uncontrolled even though its initial about:blank is controlled. | ||
let frame = await createFrameWithNestedWindow(scope); | ||
let nested = frame.contentWindow.nested(); | ||
let initialResult = nested.document.body.textContent; | ||
|
||
// The nested iframe should not have been intercepted by the service | ||
// worker. The empty.html nested frame has "hello world" for its body. | ||
assert_equals(initialResult.trim(), 'hello world', `result: ${initialResult}`); | ||
|
||
assert_not_equals(frame.contentWindow.navigator.serviceWorker.controller, null, | ||
'outer frame should be controlled'); | ||
|
||
assert_equals(nested.navigator.serviceWorker.controller, null, | ||
'nested frame should not be controlled'); | ||
|
||
frame.remove(); | ||
await service_worker_unregister_and_done(t, scope); | ||
}, 'Initial about:blank is controlled, exposed to clients.matchAll(), and ' + | ||
'final Client is not controlled by a service worker.'); | ||
|
||
</script> | ||
</body> |
31 changes: 31 additions & 0 deletions
31
service-workers/service-worker/resources/about-blank-replacement-frame.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
def main(request, response): | ||
if 'nested' in request.GET: | ||
return ( | ||
[('Content-Type', 'text/html')], | ||
'failed: nested frame was not intercepted by the service worker' | ||
) | ||
|
||
return ([('Content-Type', 'text/html')], """ | ||
<!doctype html> | ||
<html> | ||
<body> | ||
<script> | ||
function nestedLoaded() { | ||
parent.postMessage({ type: 'NESTED_LOADED' }, '*'); | ||
} | ||
</script> | ||
<iframe src="?nested=true" id="nested" onload="nestedLoaded()"></iframe> | ||
<script> | ||
// Helper routine to make it slightly easier for our parent to find | ||
// the nested frame. | ||
function nested() { | ||
return document.getElementById('nested').contentWindow; | ||
} | ||
// NOTE: Make sure not to touch the iframe directly here. We want to | ||
// test the case where the initial about:blank document is not | ||
// directly accessed before load. | ||
</script> | ||
</body> | ||
</html> | ||
""") |
48 changes: 48 additions & 0 deletions
48
service-workers/service-worker/resources/about-blank-replacement-ping-frame.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
def main(request, response): | ||
if 'nested' in request.GET: | ||
return ( | ||
[('Content-Type', 'text/html')], | ||
'failed: nested frame was not intercepted by the service worker' | ||
) | ||
|
||
return ([('Content-Type', 'text/html')], """ | ||
<!doctype html> | ||
<html> | ||
<body> | ||
<script> | ||
function nestedLoaded() { | ||
parent.postMessage({ type: 'NESTED_LOADED' }, '*'); | ||
} | ||
</script> | ||
<iframe src="?nested=true&ping=true" id="nested" onload="nestedLoaded()"></iframe> | ||
<script> | ||
// Helper routine to make it slightly easier for our parent to find | ||
// the nested frame. | ||
function nested() { | ||
return document.getElementById('nested').contentWindow; | ||
} | ||
// This modifies the nested iframe immediately and does not wait for it to | ||
// load. This effectively modifies the global for the initial about:blank | ||
// document. Any modifications made here should be preserved after the | ||
// frame loads because the global should be re-used. | ||
let win = nested(); | ||
if (win.location.href !== 'about:blank') { | ||
parent.postMessage({ | ||
type: 'NESTED_LOADED', | ||
result: 'failed: nested iframe does not have an initial about:blank URL' | ||
}, '*'); | ||
} else { | ||
win.navigator.serviceWorker.addEventListener('message', evt => { | ||
if (evt.data.type === 'PING') { | ||
evt.source.postMessage({ | ||
type: 'PONG', | ||
location: win.location.toString() | ||
}); | ||
} | ||
}); | ||
} | ||
</script> | ||
</body> | ||
</html> | ||
""") |
29 changes: 29 additions & 0 deletions
29
service-workers/service-worker/resources/about-blank-replacement-popup-frame.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
def main(request, response): | ||
if 'nested' in request.GET: | ||
return ( | ||
[('Content-Type', 'text/html')], | ||
'failed: nested frame was not intercepted by the service worker' | ||
) | ||
|
||
return ([('Content-Type', 'text/html')], """ | ||
<!doctype html> | ||
<html> | ||
<body> | ||
<script> | ||
function nestedLoaded() { | ||
parent.postMessage({ type: 'NESTED_LOADED' }, '*'); | ||
popup.close(); | ||
} | ||
let popup = window.open('?nested=true'); | ||
popup.onload = nestedLoaded; | ||
// Helper routine to make it slightly easier for our parent to find | ||
// the nested popup window. | ||
function nested() { | ||
return popup; | ||
} | ||
</script> | ||
</body> | ||
</html> | ||
""") |
22 changes: 22 additions & 0 deletions
22
...e-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<!doctype html> | ||
<html> | ||
<body> | ||
<script> | ||
function nestedLoaded() { | ||
parent.postMessage({ type: 'NESTED_LOADED' }, '*'); | ||
} | ||
</script> | ||
<iframe src="empty.html?nested=true" id="nested" onload="nestedLoaded()"></iframe> | ||
<script> | ||
// Helper routine to make it slightly easier for our parent to find | ||
// the nested frame. | ||
function nested() { | ||
return document.getElementById('nested').contentWindow; | ||
} | ||
|
||
// NOTE: Make sure not to touch the iframe directly here. We want to | ||
// test the case where the initial about:blank document is not | ||
// directly accessed before load. | ||
</script> | ||
</body> | ||
</html> |
95 changes: 95 additions & 0 deletions
95
service-workers/service-worker/resources/about-blank-replacement-worker.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// Helper routine to find a client that matches a particular URL. Note, we | ||
// require that Client to be controlled to avoid false matches with other | ||
// about:blank windows the browser might have. The initial about:blank should | ||
// inherit the controller from its parent. | ||
async function getClientByURL(url) { | ||
let list = await clients.matchAll(); | ||
return list.find(client => client.url === url); | ||
} | ||
|
||
// Helper routine to perform a ping-pong with the given target client. We | ||
// expect the Client to respond with its location URL. | ||
async function pingPong(target) { | ||
function waitForPong() { | ||
return new Promise(resolve => { | ||
self.addEventListener('message', function onMessage(evt) { | ||
if (evt.data.type === 'PONG') { | ||
resolve(evt.data.location); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
target.postMessage({ type: 'PING' }) | ||
return await waitForPong(target); | ||
} | ||
|
||
addEventListener('fetch', async evt => { | ||
let url = new URL(evt.request.url); | ||
if (!url.searchParams.get('nested')) { | ||
return; | ||
} | ||
|
||
evt.respondWith(async function() { | ||
// Find the initial about:blank document. | ||
const client = await getClientByURL('about:blank'); | ||
if (!client) { | ||
return new Response('failure: could not find about:blank client'); | ||
} | ||
|
||
// If the nested frame is configured to support a ping-pong, then | ||
// ping it now to verify its message listener exists. We also | ||
// verify the Client's idea of its own location URL while we are doing | ||
// this. | ||
if (url.searchParams.get('ping')) { | ||
const loc = await pingPong(client); | ||
if (loc !== 'about:blank') { | ||
return new Response(`failure: got location {$loc}, expected about:blank`); | ||
} | ||
} | ||
|
||
// Finally, allow the nested frame to complete loading. We place the | ||
// Client ID we found for the initial about:blank in the body. | ||
return new Response(client.id); | ||
}()); | ||
}); | ||
|
||
addEventListener('message', evt => { | ||
if (evt.data.type !== 'GET_CLIENT_ID') { | ||
return; | ||
} | ||
|
||
evt.waitUntil(async function() { | ||
let url = new URL(evt.data.url); | ||
|
||
// Find the given Client by its URL. | ||
let client = await getClientByURL(evt.data.url); | ||
if (!client) { | ||
evt.source.postMessage({ | ||
type: 'GET_CLIENT_ID', | ||
result: `failure: could not find ${evt.data.url} client` | ||
}); | ||
return; | ||
} | ||
|
||
// If the Client supports a ping-pong, then do it now to verify | ||
// the message listener exists and its location matches the | ||
// Client object. | ||
if (url.searchParams.get('ping')) { | ||
let loc = await pingPong(client); | ||
if (loc !== evt.data.url) { | ||
evt.source.postMessage({ | ||
type: 'GET_CLIENT_ID', | ||
result: `failure: got location ${loc}, expected ${evt.data.url}` | ||
}); | ||
return; | ||
} | ||
} | ||
|
||
// Finally, send the client ID back. | ||
evt.source.postMessage({ | ||
type: 'GET_CLIENT_ID', | ||
result: client.id | ||
}); | ||
}()); | ||
}); |