Skip to content

Commit

Permalink
Prerender: Upstream a basic test for SpeculationRules triggered prere…
Browse files Browse the repository at this point in the history
…ndering to WPT

This CL upstreams a basic prerender test that confirms
`document.prerendering` and `document.onprerenderingchange` event on
pages prerendered by SpeculationRules to the WPT repository.

> Directory structure

This introduces a new directory speculation-rules/ and a sub directory
speculation-rules/prerender/. Tests for other pre* features triggered by
SpeculationRules will be placed in subdirectories of speculation-rules/.

> Feature detection

The test checks if SpeculationRules is available using
`HTMLScriptElement.supports('speculationrules')`. This doesn't exactly
check if SpeculationRules triggered prerendering is available, but
currently this is the only way to (roughly) detect the feature and still
useful for avoiding timeout on other user agents.

> For VIRTUAL_OWNERS

This CL just moves the existing test to the new directory and updates
the VirtualTestSuites configuration for the directory, so this doesn't
increase the number of tests to run.

Change-Id: I3fbd729de42f560b9157ff1a3b4ba6e4e973a0d6
Bug: 1253158
  • Loading branch information
nhiroki authored and chromium-wpt-export-bot committed Sep 30, 2021
1 parent 7ac3315 commit 8dc0374
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lint.ignore
Expand Up @@ -825,3 +825,5 @@ SET TIMEOUT: editing/crashtests/backcolor-in-nested-editing-host-td-from-DOMAttr
SET TIMEOUT: editing/crashtests/inserthtml-in-text-adopted-to-other-document.html
SET TIMEOUT: editing/crashtests/insertorderedlist-in-text-adopted-to-other-document.html
SET TIMEOUT: editing/crashtests/outdent-across-svg-boundary.html

SET TIMEOUT: speculation-rules/prerender/resources/utils.js
27 changes: 27 additions & 0 deletions speculation-rules/prerender/resources/key-value-store.py
@@ -0,0 +1,27 @@
"""Key-Value store server.
The request takes "key=" and "value=" URL parameters. The key must be UUID
generated by token().
- When only the "key=" is specified, serves a 200 response whose body contains
the stored value specified by the key. If the stored value doesn't exist,
serves a 200 response with an empty body.
- When both the "key=" and "value=" are specified, stores the pair and serves
a 200 response without body.
"""


def main(request, response):
key = request.GET.get(b"key")
value = request.GET.get(b"value", None)

# Store the value.
if value:
request.server.stash.put(key, value)
return (200, [], b"")

# Get the value.
data = request.server.stash.take(key)
if not data:
return (200, [], b"")
return (200, [], data)
82 changes: 82 additions & 0 deletions speculation-rules/prerender/resources/prerender-state.html
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<script src="/common/utils.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="utils.js"></script>
<script>

// TODO(https://crbug.com/1174978): Make sure this page is being prerendered.

const params = new URLSearchParams(location.search);

// Take a key used for storing a test result in the server.
const key = params.get('key');

// The main test page (state-and-event.html in the parent directory) will load
// this page only with the "key" parameter. This page will then prerender
// itself with the "run-test" parameter. When "run-test" is in the URL we'll
// actually start the test process and record the results to send back to the
// main test page. We do this because the main test page cannot navigate itself
// but it also cannot open a popup to a prerendered browsing context so the
// prerender triggering and activation must both happen in this popup.
const run_test = params.has('run-test');
if (!run_test) {
// Generate a new stash key so we can communicate with the prerendered page
// about when to activate it.
const activate_key = token();
const url = new URL(document.URL);
url.searchParams.append('run-test', '');
url.searchParams.append('activate-key', activate_key);
startPrerendering(url.toString());

// Wait until the prerendered page signals us it's time to activate, then
// navigate to it.
nextValueFromServer(activate_key).then(() => {
window.location = url.toString();
});
} else {
const activate_key = params.get('activate-key');
const result = {
// Check the types of the members on document.
prerenderingTypeOf: typeof(document.prerendering),
onprerenderingChangeTypeOf: typeof(document.onprerenderingchange),

// Check the value of document.prerendering now and after activation.
prerenderingValueBeforeActivate: document.prerendering,
prerenderingValueAfterActivate: null,

// Track when the prerenderingchange event is fired.
onprerenderingchangeCalledBeforeActivate: false,
onprerenderingchangeCalledAfterActivate: false,

// Tracks the properties on the prerenderingchange event.
eventBubbles: null,
eventCancelable: null
};

let did_load = false;

addEventListener('load', () => {
did_load = true;

// Tell the harness we've finished loading so we can proceed to activation.
writeValueToServer(activate_key, 'did_load');
});

document.addEventListener('prerenderingchange', (e) => {
result.eventBubbles = e.bubbles;
result.eventCancelable = e.cancelable;

if (did_load) {
result.onprerenderingchangeCalledAfterActivate = true;
result.prerenderingValueAfterActivate = document.prerendering;
writeValueToServer(key, JSON.stringify(result)).then(() => {
window.close();
});
} else {
result.onprerenderingchangeCalledBeforeActivate = true;
}
});
}

</script>
145 changes: 145 additions & 0 deletions speculation-rules/prerender/resources/utils.js
@@ -0,0 +1,145 @@
const STORE_URL = '/speculation-rules/prerender/resources/key-value-store.py';

function assertSpeculationRulesIsSupported() {
assert_implements(
'supports' in HTMLScriptElement,
'HTMLScriptElement.supports is not supported');
assert_implements(
HTMLScriptElement.supports('speculationrules'),
'<script type="speculationrules"> is not supported');
}

// Starts prerendering for `url`.
function startPrerendering(url) {
// Adds <script type="speculationrules"> and specifies a prerender candidate
// for the given URL.
// TODO(https://crbug.com/1174978): <script type="speculationrules"> may not
// start prerendering for some reason (e.g., resource limit). Implement a
// WebDriver API to force prerendering.
const script = document.createElement('script');
script.type = 'speculationrules';
script.text = `{"prerender": [{"source": "list", "urls": ["${url}"] }] }`;
document.head.appendChild(script);
}

// Reads the value specified by `key` from the key-value store on the server.
async function readValueFromServer(key) {
const serverUrl = `${STORE_URL}?key=${key}`;
const response = await fetch(serverUrl);
if (!response.ok)
throw new Error('An error happened in the server');
const value = await response.text();

// The value is not stored in the server.
if (value === "")
return { status: false };

return { status: true, value: value };
}

// Convenience wrapper around the above getter that will wait until a value is
// available on the server.
async function nextValueFromServer(key) {
while (true) {
// Fetches the test result from the server.
const { status, value } = await readValueFromServer(key);
if (!status) {
// The test result has not been stored yet. Retry after a while.
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}

return value;
}
}

// Writes `value` for `key` in the key-value store on the server.
async function writeValueToServer(key, value) {
const serverUrl = `${STORE_URL}?key=${key}&value=${value}`;
await fetch(serverUrl);
}

// Loads the initiator page, and navigates to the prerendered page after it
// receives the 'readyToActivate' message.
function loadInitiatorPage() {
// Used to communicate with the prerendering page.
const prerenderChannel = new BroadcastChannel('prerender-channel');
window.addEventListener('unload', () => {
prerenderChannel.close();
});

// We need to wait for the 'readyToActivate' message before navigation
// since the prerendering implementation in Chromium can only activate if the
// response for the prerendering navigation has already been received and the
// prerendering document was created.
const readyToActivate = new Promise((resolve, reject) => {
prerenderChannel.addEventListener('message', e => {
if (e.data != 'readyToActivate')
reject(`The initiator page receives an unsupported message: ${e.data}`);
resolve(e.data);
});
});

const url = new URL(document.URL);
url.searchParams.append('prerendering', '');
// Prerender a page that notifies the initiator page of the page's ready to be
// activated via the 'readyToActivate'.
startPrerendering(url.toString());

// Navigate to the prerendered page after being informed.
readyToActivate.then(() => {
window.location = url.toString();
}).catch(e => {
const testChannel = new BroadcastChannel('test-channel');
testChannel.postMessage(
`Failed to navigate the prerendered page: ${e.toString()}`);
testChannel.close();
window.close();
});
}

// Returns messages received from the given BroadcastChannel
// so that callers do not need to add their own event listeners.
// nextMessage() returns a promise which resolves with the next message.
//
// Usage:
// const channel = new BroadcastChannel('channel-name');
// const messageQueue = new BroadcastMessageQueue(channel);
// const message1 = await messageQueue.nextMessage();
// const message2 = await messageQueue.nextMessage();
// message1 and message2 are the messages received.
class BroadcastMessageQueue {
constructor(broadcastChannel) {
this.messages = [];
this.resolveFunctions = [];
this.channel = broadcastChannel;
this.channel.addEventListener('message', e => {
if (this.resolveFunctions.length > 0) {
const fn = this.resolveFunctions.shift();
fn(e.data);
} else {
this.messages.push(e.data);
}
});
}

// Returns a promise that resolves with the next message from this queue.
nextMessage() {
return new Promise(resolve => {
if (this.messages.length > 0)
resolve(this.messages.shift())
else
this.resolveFunctions.push(resolve);
});
}
}

// Returns <iframe> element upon load.
function createFrame(url) {
return new Promise(resolve => {
const frame = document.createElement('iframe');
frame.src = url;
frame.onload = () => resolve(frame);
document.body.appendChild(frame);
});
}
48 changes: 48 additions & 0 deletions speculation-rules/prerender/state-and-event.html
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<meta name="timeout" content="long">
<script src="/common/utils.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/utils.js"></script>
<body>
<script>

setup(() => assertSpeculationRulesIsSupported());

promise_test(async t => {
// The key used for storing a test result in the server.
const key = token();

// Open the test runner in a popup - it will prerender itself, record the
// test results, and send them back to this harness.
const url = `resources/prerender-state.html?key=${key}`;
window.open(url, '_blank', 'noopener');

// Wait until the test sends us the results.
let result = await nextValueFromServer(key);
result = JSON.parse(result);

assert_equals(result.prerenderingTypeOf, "boolean",
"typeof(document.prerendering) is 'boolean'.");
assert_equals(result.onprerenderingChangeTypeOf, "object",
"typeof(document.onprerenderingchange) is 'object'.");

assert_equals(
result.onprerenderingchangeCalledBeforeActivate, false,
"prerenderingchange event should not be called prior to activation.");
assert_equals(
result.prerenderingValueBeforeActivate, true,
"document.prerendering should be true prior to activation.");

assert_equals(result.onprerenderingchangeCalledAfterActivate, true,
"prerenderingchange event should be called after activation.");
assert_equals(result.prerenderingValueAfterActivate, false,
"document.prerendering should be false after activation.");
assert_equals(result.eventBubbles, false,
"prerenderingchange event.bubbles should be false.");
assert_equals(result.eventCancelable, false,
"prerenderingchange event.cancelable should be false.");
}, 'Test document.prerendering and its change event.');

</script>
</body>

0 comments on commit 8dc0374

Please sign in to comment.