-
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
back-forward-cache/resources/helper.sub.js: Helper for A->B->A navigation scenarios. BFCache state is detected by observing `pageshow` events. We might want to use more explicit APIs like `isPreviousPageInBFCache` discussed at #16359 in the future / in more complicated scenarios, but so far `pageshow`-based detection seems to work without hurting ergonomics. back-forward-cache/resources/events.html: The file that loads `helper.sub.js` and contains test logic and assertions. We navigate to `back-forward-cache/resources/back.html` and then back-navigate to `back-forward-cache/resources/events.html`. In the case of BFCache is not used, two async_test objects are created: - The first one during the initial navigation, which never completes, and - The second one during the back navigation, which will execute test assertions and then complete. This doesn't affect the behavior (the first one seems just ignored), but might look awkward. back-forward-cache/events.html: The main test Document that opens `back-forward-cache/resources/events.html` using `window.open()` with 'noopener' option. This is because (at least) Chromium requires top-level navigations to trigger BFCache and thus `back-forward-cache/resources/events.html` is navigated away during the test, but the WPT test infrastructure doesn't support navigating the main test Document. testharness.js: `fetch_tests_from_prefixed_local_storage()` is introduced to communicate test results from `back-forward-cache/resources/events.html` in a similar way to `fetch_tests_from_window()`. PrefixedLocalStorage.js: Some basic utility methods for `helper.sub.js` are added. Design doc: https://docs.google.com/document/d/1p3G-qNYMTHf5LU9hykaXcYtJ0k3wYOwcdVKGeps6EkU/edit?usp=sharing Bug: 1107415 Change-Id: I034f9f5376dc3f9f32ca0b936dbd06e458c9160b
- Loading branch information
1 parent
5d10b8c
commit 5e2f7cb
Showing
6 changed files
with
276 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. |
34 changes: 34 additions & 0 deletions
34
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,34 @@ | ||
<!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'); | ||
|
||
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> |
94 changes: 94 additions & 0 deletions
94
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,94 @@ | ||
// 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); | ||
} | ||
|
||
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}`); | ||
} | ||
} | ||
|
||
// Asserts that the executor with uuid `idTarget` is (or isn't, respectively) | ||
// restored from BFCache. | ||
// These should be used with prepareNavigation() (see `../README.md`). | ||
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'); | ||
} |
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