-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BFCache] Add basic event tests + helpers for BFCache WPT
Design doc: https://docs.google.com/document/d/1p3G-qNYMTHf5LU9hykaXcYtJ0k3wYOwcdVKGeps6EkU/edit?usp=sharing Bug: 1107415 Change-Id: I034f9f5376dc3f9f32ca0b936dbd06e458c9160b
- Loading branch information
1 parent
f6d8412
commit 1a20d7c
Showing
6 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
31 changes: 31 additions & 0 deletions
31
html/browsers/browsing-the-web/back-forward-cache/README.md
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 @@ | ||
# Dispatcher/executor framework | ||
|
||
In the BFCache tests, the main test HTML | ||
|
||
1. Opens new executor Windows using `window.open()` + `noopener` option, and | ||
2. Injects scripts to / receives values from the executor Windows via send()/receive() methods provided by | ||
[the dispatcher/executor framework of COEP credentialless](../../../cross-origin-embedder-policy/credentialless/README.md) | ||
|
||
because less isolated Windows (e.g. iframes and `window.open()` without `noopener` option) are often not eligible for BFCache (e.g. in Chromium). | ||
|
||
# BFCache-specific helpers | ||
|
||
- [resources/executor.html](resources/executor.html) is the BFCache-specific executor and contains helpers for executors. | ||
- [resources/helper.sub.js](resources/helper.sub.js) contains helpers for main test HTMLs. | ||
|
||
In typical A-B-A scenarios (where we navigate from Page A to Page B and then navigate back to Page A, assuming Page A is (or isn't) in BFCache), | ||
|
||
- Call `prepareNavigation()` on the executor, and then navigate to B, and then navigate back to Page A. | ||
- Call `assert_bfcached()` or `assert_not_bfcached()` on the main test HTML, to check the BFCache status. | ||
- Check other test expectations on the main test HTML. | ||
|
||
Note that | ||
|
||
- `await`ing `send()` calls (and other wrapper methods) is needed to serialize injected scripts. | ||
- `send()`/`receive()` uses Fetch API + server-side stash. | ||
`prepareNavigation()` suspends Fetch API calls until we navigate back to the page, to avoid conflicts with BFCache eligibility. | ||
|
||
# Asserting PRECONDITION_FAILED for unexpected BFCache eligibility | ||
|
||
To distinguish failures due to unexpected BFCache eligibility (which might be acceptable due to different BFCache eligibility criteria across browsers), | ||
`assert_bfcached()` and `assert_not_bfcached()` asserts `PRECONDITION_FAILED` rather than ordinal failures. |
35 changes: 35 additions & 0 deletions
35
html/browsers/browsing-the-web/back-forward-cache/events.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,35 @@ | ||
<!DOCTYPE HTML> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script src="/common/utils.js"></script> | ||
<script src="/html/cross-origin-embedder-policy/credentialless/resources/dispatcher.js"></script> | ||
<script src="resources/helper.sub.js"></script> | ||
<script> | ||
for (const originType of ['SameOrigin', 'SameSite', 'CrossSite']) { | ||
promise_test(async t => { | ||
const idA = token(); | ||
|
||
window.open( | ||
executorPath + idA + '&events=pagehide,pageshow,load', | ||
'_blank', 'noopener'); | ||
|
||
// Navigate to a page that immediately triggers back navigation. | ||
const backUrl = eval(`origin${originType}`) + backPath; | ||
await send(idA, ` | ||
prepareNavigation(); | ||
location.href = '${backUrl}'; | ||
`); | ||
|
||
await assert_bfcached(idA); | ||
|
||
assert_array_equals( | ||
await evalOn(idA, `getRecordedEvents()`), | ||
[ | ||
'window.load', | ||
'window.pageshow', | ||
'window.pagehide.persisted', | ||
'window.pageshow.persisted' | ||
]); | ||
}, `Events fired (window.open + noopener, ${originType})`); | ||
} | ||
</script> |
4 changes: 4 additions & 0 deletions
4
html/browsers/browsing-the-web/back-forward-cache/resources/back.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,4 @@ | ||
<!DOCTYPE HTML> | ||
<script> | ||
window.onload = () => history.back(); | ||
</script> |
112 changes: 112 additions & 0 deletions
112
html/browsers/browsing-the-web/back-forward-cache/resources/executor.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,112 @@ | ||
<!DOCTYPE HTML> | ||
<script src="/html/cross-origin-embedder-policy/credentialless/resources/dispatcher.js"></script> | ||
<script> | ||
const params = new URLSearchParams(window.location.search); | ||
const uuid = params.get('uuid'); | ||
|
||
// -------- | ||
// Recording events | ||
|
||
// The recorded events are stored in localStorage rather than global variables | ||
// to catch events fired just before navigating out. | ||
function getPushedItems(key) { | ||
return JSON.parse(localStorage.getItem(key) || '[]'); | ||
} | ||
|
||
function pushItem(key, value) { | ||
const array = getPushedItems(key); | ||
array.push(value); | ||
localStorage.setItem(key, JSON.stringify(array)); | ||
} | ||
|
||
function recordEvent(eventName) { | ||
pushItem(uuid + '.observedEvents', eventName); | ||
} | ||
|
||
function getRecordedEvents() { | ||
return getPushedItems(uuid + '.observedEvents'); | ||
} | ||
|
||
// Records events fired on `window` and `document`, with names listed in | ||
// `eventNames`. | ||
function startRecordingEvents(eventNames) { | ||
for (const eventName of eventNames) { | ||
window.addEventListener(eventName, event => { | ||
let result = eventName; | ||
if (event.persisted) { | ||
result += '.persisted'; | ||
} | ||
if (eventName === 'visibilitychange') { | ||
result += '.' + document.visibilityState; | ||
} | ||
recordEvent('window.' + result); | ||
}); | ||
document.addEventListener(eventName, () => { | ||
let result = eventName; | ||
if (eventName === 'visibilitychange') { | ||
result += '.' + document.visibilityState; | ||
} | ||
recordEvent('document.' + result); | ||
}); | ||
} | ||
} | ||
|
||
// When a comma-separated list of event names are given as the `events` | ||
// parameter in the URL, start record the events of the given names. | ||
if (params.get('events')) { | ||
startRecordingEvents(params.get('events').split(',')); | ||
} | ||
|
||
// -------- | ||
// Executor and BFCache detection | ||
|
||
// When navigating out from this page and then back navigating, | ||
// call prepareNavigation() immediately before navigating out. | ||
// | ||
// In such scenarios, `assert_bfcached()` etc. in `helper.sub.js` can determine | ||
// whether the page is restored from BFCache or not, by observing | ||
// - isPageshowFired: whether the pageshow event listener added by the | ||
// prepareNavigation() before navigating out, and | ||
// - loadCount: whether this inline script is evaluated again | ||
|
||
// prepareNavigation() also suspends task polling, to avoid in-flight fetch | ||
// requests during navigation that might evict the page from BFCache. | ||
// Task polling is resumed later | ||
// - (BFCache cases) when the pageshow event listener added by | ||
// prepareNavigation() is executed, or | ||
// - (Non-BFCache cases) when executeOrders() is called again during | ||
// non-BFCache page loading. | ||
|
||
window.isPageshowFired = false; | ||
|
||
window.shouldSuspendFetch = false; | ||
|
||
window.loadCount = parseInt(localStorage.getItem(uuid + '.loadCount') || '0') + 1; | ||
localStorage.setItem(uuid + '.loadCount', loadCount); | ||
|
||
function prepareNavigation() { | ||
window.shouldSuspendFetch = true; | ||
window.addEventListener( | ||
'pageshow', | ||
() => { | ||
window.isPageshowFired = true; | ||
window.shouldSuspendFetch = false; | ||
}, | ||
{once: true}); | ||
} | ||
|
||
// Tasks are executed after a pageshow event is fired. | ||
window.addEventListener('pageshow', () => { | ||
const executeOrders = async function() { | ||
while (true) { | ||
if (!window.shouldSuspendFetch) { | ||
const task = await receive(uuid); | ||
await eval(`(async () => {${task}})()`); | ||
} | ||
await new Promise(resolve => setTimeout(resolve, 100)); | ||
} | ||
}; | ||
executeOrders(); | ||
}, | ||
{once: true}); | ||
</script> |
104 changes: 104 additions & 0 deletions
104
html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.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,104 @@ | ||
// Helpers called on the main test HTMLs. | ||
// Scripts in `send()` arguments are evaluated on the executors | ||
// (`executor.html`), and helpers available on the executors are defined in | ||
// `executor.html`. | ||
|
||
const idThis = token(); | ||
|
||
const originSameOrigin = | ||
location.protocol === 'http:' ? | ||
'http://{{host}}:{{ports[http][0]}}' : | ||
'https://{{host}}:{{ports[https][0]}}'; | ||
const originSameSite = | ||
location.protocol === 'http:' ? | ||
'http://{{host}}:{{ports[http][1]}}' : | ||
'https://{{host}}:{{ports[https][1]}}'; | ||
const originCrossSite = | ||
location.protocol === 'http:' ? | ||
'http://{{hosts[alt][www]}}:{{ports[http][0]}}' : | ||
'https://{{hosts[alt][www]}}:{{ports[https][0]}}'; | ||
|
||
const executorPath = | ||
'/html/browsers/browsing-the-web/back-forward-cache/resources/executor.html?uuid='; | ||
const backPath = | ||
'/html/browsers/browsing-the-web/back-forward-cache/resources/back.html'; | ||
|
||
// On the executor with uuid `idTarget`: Evaluates the script `expr`, and | ||
// On the caller: returns a Promise resolved with the result of `expr`. | ||
// This assumes the result can be serialized by JSON.stringify(). | ||
async function evalOn(idTarget, expr) { | ||
await send(idTarget, `await send('${idThis}', JSON.stringify(${expr}));`); | ||
const result = await receive(idThis); | ||
return JSON.parse(result); | ||
} | ||
|
||
// On the executor with uuid `idTarget`: | ||
// Evaluates `script` that returns a Promise resolved with `result`. | ||
// On the caller: | ||
// Returns a Promise resolved with `result` | ||
// (or 'Error' when the promise is rejected). | ||
// This assumes `result` can be serialized by JSON.stringify(). | ||
async function asyncEvalOn(idTarget, script) { | ||
send(idTarget, ` | ||
try { | ||
const result = await async function() { ${script} }(); | ||
await send('${idThis}', JSON.stringify(result)); | ||
} | ||
catch (error) { | ||
await send('${idThis}', '"Error"'); | ||
}`); | ||
const result = await receive(idThis); | ||
return JSON.parse(result); | ||
} | ||
|
||
async function runEligibilityCheck(script) { | ||
const idA = token(); | ||
window.open(executorPath + idA, '_blank', 'noopener'); | ||
await send(idA, script); | ||
await send(idA, ` | ||
prepareNavigation(); | ||
location.href = '${originCrossSite + backPath}'; | ||
`); | ||
await assert_bfcached(idA); | ||
} | ||
|
||
// Asserts that the executor with uuid `idTarget` is (or isn't, respectively) | ||
// restored from BFCache. These should be used in the following fashion: | ||
// 1. Call prepareNavigation() on the executor `idTarget` using send(). | ||
// 2. Navigate the executor to another page. | ||
// 3. Navigate back to the executor `idTarget`. | ||
// 4. Call assert_bfcached() or assert_not_bfcached() on the main test HTML. | ||
// | ||
// These methods (and getBFCachedStatus()) should be called after the send() | ||
// Promise in Step 1 is resolved, but we don't need to wait for the completion | ||
// of the navigation and back navigation in Steps 2 and 3, | ||
// because the injected scripts to the executor are queued and aren't executed | ||
// between prepareNavigation() and the completion of the back navigation. | ||
async function assert_bfcached(idTarget) { | ||
const status = await getBFCachedStatus(idTarget); | ||
assert_implements_optional(status === 'BFCached', 'Should be BFCached'); | ||
} | ||
|
||
async function assert_not_bfcached(idTarget) { | ||
const status = await getBFCachedStatus(idTarget); | ||
assert_implements_optional(status !== 'BFCached', 'Should not be BFCached'); | ||
} | ||
|
||
async function getBFCachedStatus(idTarget) { | ||
const [loadCount, isPageshowFired] = | ||
await evalOn(idTarget, '[window.loadCount, window.isPageshowFired]'); | ||
if (loadCount === 1 && isPageshowFired === true) { | ||
return 'BFCached'; | ||
} else if (loadCount === 2 && isPageshowFired === false) { | ||
return 'Not BFCached'; | ||
} else { | ||
// This can occur for example when this is called before first navigating | ||
// away (loadCount = 1, isPageshowFired = false), e.g. when | ||
// 1. sending a script for navigation and then | ||
// 2. calling getBFCachedStatus() without waiting for the completion of | ||
// the script on the `idTarget` page. | ||
assert_unreached( | ||
`Got unexpected BFCache status: loadCount = ${loadCount}, ` + | ||
`isPageshowFired = ${isPageshowFired}`); | ||
} | ||
} |
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