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
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 9e8ad69
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 9e8ad69

Please sign in to comment.