Skip to content

Commit

Permalink
[BFCache] Add basic event tests + helpers for BFCache WPT
Browse files Browse the repository at this point in the history
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
hiroshige-g authored and chromium-wpt-export-bot committed Jul 9, 2021
1 parent 5d10b8c commit 5e2f7cb
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 0 deletions.
31 changes: 31 additions & 0 deletions html/browsers/browsing-the-web/back-forward-cache/README.md
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 html/browsers/browsing-the-web/back-forward-cache/events.html
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!DOCTYPE HTML>
<script>
window.onload = () => history.back();
</script>
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>
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');
}
1 change: 1 addition & 0 deletions lint.ignore
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ SET TIMEOUT: resources/test/tests/unit/promise_setup.html
SET TIMEOUT: resources/testharness.js
SET TIMEOUT: scheduler/tentative/current-task-signal-async-abort.any.js
SET TIMEOUT: scheduler/tentative/current-task-signal-async-priority.any.js
SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/resources/executor.html

# setTimeout use in reftests
SET TIMEOUT: acid/acid3/test.html
Expand Down

0 comments on commit 5e2f7cb

Please sign in to comment.