summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/pending-beacon
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/pending-beacon')
-rw-r--r--testing/web-platform/tests/pending-beacon/META.yml4
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_beacon-basic.tentative.https.window.js87
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_beacon-basic.window.js9
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_beacon-deactivate.tentative.https.window.js12
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_beacon-sendnow.tentative.https.window.js44
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_beacon-sendondiscard.tentative.https.window.js95
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_beacon-sendonhidden.tentative.https.window.js81
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_get_beacon-cors.tentative.https.window.js28
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_get_beacon-send.tentative.https.window.js38
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_post_beacon-cors.tentative.https.window.js66
-rw-r--r--testing/web-platform/tests/pending-beacon/pending_post_beacon-sendwithdata.tentative.https.window.js43
-rw-r--r--testing/web-platform/tests/pending-beacon/resources/get_beacon.py30
-rw-r--r--testing/web-platform/tests/pending-beacon/resources/pending_beacon-helper.js242
-rw-r--r--testing/web-platform/tests/pending-beacon/resources/set_beacon.py83
14 files changed, 862 insertions, 0 deletions
diff --git a/testing/web-platform/tests/pending-beacon/META.yml b/testing/web-platform/tests/pending-beacon/META.yml
new file mode 100644
index 0000000000..04b88b5f72
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/META.yml
@@ -0,0 +1,4 @@
+spec: https://wicg.github.io/pending-beacon/
+suggested_reviewers:
+ - mingyc
+ - clelland
diff --git a/testing/web-platform/tests/pending-beacon/pending_beacon-basic.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_beacon-basic.tentative.https.window.js
new file mode 100644
index 0000000000..c63ae4e39d
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_beacon-basic.tentative.https.window.js
@@ -0,0 +1,87 @@
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+test(() => {
+ assert_throws_js(TypeError, () => new PendingBeacon('/'));
+}, `PendingBeacon cannot be constructed directly.`);
+
+for (const beaconType of BeaconTypes) {
+ test(() => {
+ assert_throws_js(TypeError, () => new beaconType.type());
+ assert_throws_js(TypeError, () => new beaconType.type(undefined));
+ assert_throws_js(TypeError, () => new beaconType.type(null));
+ }, `${beaconType.name}: constructor throws TypeError if URL is missing.`);
+
+ test(() => {
+ assert_throws_js(
+ TypeError, () => new beaconType.type('http://www.google.com'));
+ assert_throws_js(TypeError, () => new beaconType.type('file://tmp'));
+ assert_throws_js(TypeError, () => new beaconType.type('ssh://example.com'));
+ assert_throws_js(TypeError, () => new beaconType.type('wss://example.com'));
+ assert_throws_js(TypeError, () => new beaconType.type('about:blank'));
+ assert_throws_js(
+ TypeError, () => new beaconType.type(`javascript:alert('');`));
+ }, `${beaconType.name}: constructor throws TypeError on non-HTTPS URL.`);
+
+ test(() => {
+ const beacon = new beaconType.type('/');
+ assert_equals(beacon.url, '/');
+ assert_equals(beacon.method, beaconType.expectedMethod);
+ assert_equals(beacon.backgroundTimeout, -1);
+ assert_equals(beacon.timeout, -1);
+ assert_true(beacon.pending);
+ }, `${beaconType.name}: constructor sets default properties if missing.`);
+
+ test(() => {
+ const beacon = new beaconType.type(
+ 'https://www.google.com', {backgroundTimeout: 200, timeout: 100});
+ assert_equals(beacon.url, 'https://www.google.com');
+ assert_equals(beacon.method, beaconType.expectedMethod);
+ assert_equals(beacon.backgroundTimeout, 200);
+ assert_equals(beacon.timeout, 100);
+ assert_true(beacon.pending);
+ }, `${beaconType.name}: constructor can set properties.`);
+
+ test(() => {
+ let beacon = new beaconType.type(
+ 'https://www.google.com',
+ {method: 'GET', backgroundTimeout: 200, timeout: 100});
+
+ beacon.backgroundTimeout = 400;
+ assert_equals(beacon.backgroundTimeout, 400);
+
+ beacon.timeout = 600;
+ assert_equals(beacon.timeout, 600);
+ }, `${beaconType.name}: 'backgroundTimeout' & 'timeout' can be mutated.`);
+
+ test(
+ () => {
+ let beacon = new beaconType.type('https://www.google.com');
+
+ assert_throws_js(TypeError, () => beacon.url = '/');
+ assert_throws_js(TypeError, () => beacon.method = 'FOO');
+ assert_throws_js(TypeError, () => beacon.pending = false);
+ },
+ `${beaconType.name}: throws TypeError when mutating ` +
+ `'url', 'method', 'pending'.`);
+}
+
+test(() => {
+ let beacon = new PendingGetBeacon('/');
+
+ assert_throws_js(TypeError, () => new beacon.setURL());
+ assert_throws_js(TypeError, () => new beacon.setURL(undefined));
+ assert_throws_js(TypeError, () => new beacon.setURL(null));
+}, `PendingGetBeacon: setURL() throws TypeError if URL is missing.`);
+
+test(() => {
+ let beacon = new PendingGetBeacon('/');
+
+ assert_throws_js(TypeError, () => beacon.setURL('http://www.google.com'));
+ assert_throws_js(TypeError, () => beacon.setURL('file://tmp'));
+ assert_throws_js(TypeError, () => beacon.setURL('ssh://example.com'));
+ assert_throws_js(TypeError, () => beacon.setURL('wss://example.com'));
+ assert_throws_js(TypeError, () => beacon.setURL('about:blank'));
+ assert_throws_js(TypeError, () => beacon.setURL(`javascript:alert('');`));
+}, `PendingGetBeacon: setURL() throws TypeError on non-HTTPS URL.`);
diff --git a/testing/web-platform/tests/pending-beacon/pending_beacon-basic.window.js b/testing/web-platform/tests/pending-beacon/pending_beacon-basic.window.js
new file mode 100644
index 0000000000..d6afd9fb5e
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_beacon-basic.window.js
@@ -0,0 +1,9 @@
+'use strict';
+
+test(() => {
+ assert_false(window.hasOwnProperty('PendingGetBeacon'));
+}, `PendingGetBeacon is not supported in non-secure context.`);
+
+test(() => {
+ assert_false(window.hasOwnProperty('PendingPostBeacon'));
+}, `PendingPostBeacon is not supported in non-secure context.`);
diff --git a/testing/web-platform/tests/pending-beacon/pending_beacon-deactivate.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_beacon-deactivate.tentative.https.window.js
new file mode 100644
index 0000000000..74c0852adc
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_beacon-deactivate.tentative.https.window.js
@@ -0,0 +1,12 @@
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+for (const beaconType of BeaconTypes) {
+ test(() => {
+ const beacon = new beaconType.type('https://www.google.com');
+ assert_true(beacon.pending);
+ beacon.deactivate();
+ assert_false(beacon.pending);
+ }, `${beaconType.name}: deactivate() changes 'pending' state.`);
+}
diff --git a/testing/web-platform/tests/pending-beacon/pending_beacon-sendnow.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_beacon-sendnow.tentative.https.window.js
new file mode 100644
index 0000000000..00baccd0c1
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_beacon-sendnow.tentative.https.window.js
@@ -0,0 +1,44 @@
+// META: script=/common/utils.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+promise_test(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Create and send a beacon.
+ const beacon = new PendingGetBeacon(url);
+ beacon.sendNow();
+
+ await expectBeacon(uuid, {count: 1});
+}, 'sendNow() sends a beacon immediately.');
+
+promise_test(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Create and send a beacon.
+ const beacon = new PendingGetBeacon(url);
+ beacon.sendNow();
+ await expectBeacon(uuid, {count: 1});
+
+ // Try to send the beacon again, and verify no beacon arrives.
+ beacon.sendNow();
+ await expectBeacon(uuid, {count: 1});
+}, 'sendNow() doesn\'t send the same beacon twice.');
+
+promise_test(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Create and send 1st beacon.
+ const beacon1 = new PendingGetBeacon(url);
+ beacon1.sendNow();
+
+ // Create and send 2st beacon.
+ const beacon2 = new PendingGetBeacon(url);
+ beacon2.sendNow();
+
+ await expectBeacon(uuid, {count: 2});
+}, 'sendNow() sends multiple beacons.');
diff --git a/testing/web-platform/tests/pending-beacon/pending_beacon-sendondiscard.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_beacon-sendondiscard.tentative.https.window.js
new file mode 100644
index 0000000000..b4283cecef
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_beacon-sendondiscard.tentative.https.window.js
@@ -0,0 +1,95 @@
+// META: script=/common/utils.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const numPerMethod = 20;
+ const total = numPerMethod * 2;
+
+ // Loads an iframe that creates `numPerMethod` GET & POST beacons.
+ const iframe = await loadScriptAsIframe(`
+ const url = "${url}";
+ for (let i = 0; i < ${numPerMethod}; i++) {
+ let get = new PendingGetBeacon(url);
+ let post = new PendingPostBeacon(url);
+ }
+ `);
+
+ // Delete the iframe to trigger beacon sending.
+ document.body.removeChild(iframe);
+
+ // The iframe should have sent all beacons.
+ await expectBeacon(uuid, {count: total});
+}, 'A discarded document sends all its beacons with default config.');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Loads an iframe that creates a GET beacon,
+ // then sends it out with `sendNow()`.
+ const iframe = await loadScriptAsIframe(`
+ const url = "${url}";
+ let beacon = new PendingGetBeacon(url);
+ beacon.sendNow();
+ `);
+
+ // Delete the document and verify no more beacons are sent.
+ document.body.removeChild(iframe);
+
+ // The iframe should have sent only 1 beacon.
+ await expectBeacon(uuid, {count: 1});
+}, 'A discarded document does not send an already sent beacon.');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const numPerMethod = 20;
+ const total = numPerMethod * 2;
+
+ // Loads an iframe that creates `numPerMethod` GET & POST beacons with
+ // different timeouts.
+ const iframe = await loadScriptAsIframe(`
+ const url = "${url}";
+ for (let i = 0; i < ${numPerMethod}; i++) {
+ let get = new PendingGetBeacon(url, {timeout: 100*i});
+ let post = new PendingPostBeacon(url, {timeout: 100*i});
+ }
+ `);
+
+ // Delete the iframe to trigger beacon sending.
+ document.body.removeChild(iframe);
+
+ // Even beacons are configured with different timeouts,
+ // the iframe should have sent all beacons when it is discarded.
+ await expectBeacon(uuid, {count: total});
+}, `A discarded document sends all its beacons of which timeouts are not
+ default.`);
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const numPerMethod = 20;
+ const total = numPerMethod * 2;
+
+ // Loads an iframe that creates `numPerMethod` GET & POST beacons with
+ // different backgroundTimeouts.
+ const iframe = await loadScriptAsIframe(`
+ const url = "${url}";
+ for (let i = 0; i < ${numPerMethod}; i++) {
+ let get = new PendingGetBeacon(url, {backgroundTimeout: 100*i});
+ let post = new PendingPostBeacon(url, {backgroundTimeout: 100*i});
+ }
+ `);
+
+ // Delete the iframe to trigger beacon sending.
+ document.body.removeChild(iframe);
+
+ // Even beacons are configured with different backgroundTimeouts,
+ // the iframe should have sent all beacons when it is discarded.
+ await expectBeacon(uuid, {count: total});
+}, `A discarded document sends all its beacons of which backgroundTimeouts are
+ not default.`);
diff --git a/testing/web-platform/tests/pending-beacon/pending_beacon-sendonhidden.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_beacon-sendonhidden.tentative.https.window.js
new file mode 100644
index 0000000000..a0ede5dadd
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_beacon-sendonhidden.tentative.https.window.js
@@ -0,0 +1,81 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+for (const beaconType of BeaconTypes) {
+ const beaconName = beaconType.name;
+
+ parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // backgroundTimeout = 0s means `beacon should be sent out right on
+ // entering `hidden` state after navigating away.
+ const options = {backgroundTimeout: 0};
+ const helper = new RemoteContextHelper();
+ // Opens a window with noopener so that BFCache will work.
+ const rc1 = await helper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // Creates a PendingBeacon in remote which should only be sent on navigating
+ // away.
+ await rc1.executeScript((beaconName, url, options) => {
+ const beacon = beaconName == 'PendingGetBeacon' ?
+ new PendingGetBeacon(url, options) :
+ new PendingPostBeacon(url, options);
+ }, [beaconName, url, options]);
+
+ await expectBeacon(uuid, {count: 0});
+ }, `${beaconName}: does not send without page navigation.`);
+
+ parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // backgroundTimeout = 0s means `beacon should be sent out right on
+ // entering `hidden` state after navigating away.
+ const options = {backgroundTimeout: 0};
+ const helper = new RemoteContextHelper();
+ // Opens a window with noopener so that BFCache will work.
+ const rc1 = await helper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // Creates a PendingBeacon in remote which should only be sent on navigating
+ // away.
+ await rc1.executeScript((beaconName, url, options) => {
+ const beacon = beaconName == 'PendingGetBeacon' ?
+ new PendingGetBeacon(url, options) :
+ new PendingPostBeacon(url, options);
+ }, [beaconName, url, options]);
+ // Navigates away to trigger beacon sending.
+ rc1.navigateToNew();
+
+ await expectBeacon(uuid, {count: 1});
+ }, `${beaconName}: sends on page entering hidden state (w/ BFCache).`);
+
+ parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // backgroundTimeout = 0s means `beacon should be sent out right on
+ // entering `hidden` state after navigating away.
+ const options = {backgroundTimeout: 0};
+ const helper = new RemoteContextHelper();
+ // Opens a window without BFCache.
+ const rc1 = await helper.addWindow();
+
+ // Creates a PendingBeacon in remote which should only be sent on navigating
+ // away.
+ await rc1.executeScript((beaconName, url, options) => {
+ const beacon = beaconName == 'PendingGetBeacon' ?
+ new PendingGetBeacon(url, options) :
+ new PendingPostBeacon(url, options);
+ }, [beaconName, url, options]);
+ // Navigates away to trigger beacon sending.
+ rc1.navigateToNew();
+
+ await expectBeacon(uuid, {count: 1});
+ }, `${beaconName}: sends on page entering hidden state (w/o BFCache).`);
+}
diff --git a/testing/web-platform/tests/pending-beacon/pending_get_beacon-cors.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_get_beacon-cors.tentative.https.window.js
new file mode 100644
index 0000000000..10bb3a0bed
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_get_beacon-cors.tentative.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info();
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN});
+
+ let beacon = new PendingGetBeacon(url);
+ beacon.sendNow();
+
+ await expectBeacon(uuid, {count: 1});
+}, 'PendingGetBeacon: same-origin');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(
+ uuid, {host: HTTPS_NOTSAMESITE_ORIGIN, expectOrigin: HTTPS_ORIGIN});
+
+ let beacon = new PendingGetBeacon(url);
+ beacon.sendNow();
+
+ await expectBeacon(uuid, {count: 1});
+}, 'PendingGetBeacon: cross-origin');
diff --git a/testing/web-platform/tests/pending-beacon/pending_get_beacon-send.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_get_beacon-send.tentative.https.window.js
new file mode 100644
index 0000000000..8c6e826af5
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_get_beacon-send.tentative.https.window.js
@@ -0,0 +1,38 @@
+// META: script=/common/utils.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+const baseUrl = `${location.protocol}//${location.host}`;
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ let beacon = new PendingGetBeacon('/');
+
+ beacon.setURL(url);
+ assert_equals(beacon.url, url);
+ beacon.sendNow();
+
+ await expectBeacon(uuid, {count: 1});
+}, 'PendingGetBeacon is sent to the updated URL');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ let beacon = new PendingGetBeacon('/0');
+
+ for (let i = 0; i < 10; i++) {
+ const transientUrl = `/${i}`;
+ beacon.setURL(transientUrl);
+ assert_equals(beacon.url, transientUrl);
+ }
+ beacon.setURL(url);
+ assert_equals(beacon.url, url);
+
+ beacon.sendNow();
+
+ await expectBeacon(uuid, {count: 1});
+}, 'PendingGetBeacon is sent to the last updated URL');
diff --git a/testing/web-platform/tests/pending-beacon/pending_post_beacon-cors.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_post_beacon-cors.tentative.https.window.js
new file mode 100644
index 0000000000..01511d22c1
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_post_beacon-cors.tentative.https.window.js
@@ -0,0 +1,66 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info();
+const SMALL_SIZE = 500;
+
+for (const dataType in BeaconDataType) {
+ postBeaconSendDataTest(
+ dataType, generatePayload(SMALL_SIZE),
+ `PendingPostBeacon[${dataType}]: same-origin`,
+ {urlOptions: {host: HTTPS_ORIGIN, expectOrigin: HTTPS_ORIGIN}});
+
+ postBeaconSendDataTest(
+ dataType, generatePayload(SMALL_SIZE),
+ `PendingPostBeacon[${dataType}]: cross-origin, ` +
+ `CORS-safelisted Content-Type`,
+ {
+ urlOptions: {
+ host: HTTPS_NOTSAMESITE_ORIGIN,
+ expectOrigin: HTTPS_ORIGIN,
+ }
+ });
+
+ postBeaconSendDataTest(
+ dataType, generatePayload(SMALL_SIZE),
+ `PendingPostBeacon[${dataType}]: cross-origin, ` +
+ 'CORS-safelisted Content-Type => ' +
+ 'non-CORS response (from redirect handler) ' +
+ 'should be rejected by browser',
+ {
+ expectCount: 0,
+ urlOptions: {
+ useRedirectHandler: true,
+ host: HTTPS_NOTSAMESITE_ORIGIN,
+ }
+ });
+
+ postBeaconSendDataTest(
+ dataType, generatePayload(SMALL_SIZE),
+ `PendingPostBeacon[${dataType}]: cross-origin, ` +
+ 'CORS-safelisted Content-Type => no cookie expected',
+ {
+ setCookie: 'test_beacon_cookie',
+ urlOptions: {
+ host: HTTPS_NOTSAMESITE_ORIGIN,
+ expectOrigin: HTTPS_ORIGIN,
+ expectCredentials: false,
+ }
+ });
+}
+
+postBeaconSendDataTest(
+ BeaconDataType.Blob, generatePayload(SMALL_SIZE),
+ 'PendingPostBeacon[Blob]: cross-origin, non-CORS-safelisted Content-Type' +
+ ' => preflight expected',
+ {
+ urlOptions: {
+ host: HTTPS_NOTSAMESITE_ORIGIN,
+ contentType: 'application/octet-stream',
+ expectOrigin: HTTPS_ORIGIN,
+ expectPreflight: true,
+ }
+ });
diff --git a/testing/web-platform/tests/pending-beacon/pending_post_beacon-sendwithdata.tentative.https.window.js b/testing/web-platform/tests/pending-beacon/pending_post_beacon-sendwithdata.tentative.https.window.js
new file mode 100644
index 0000000000..77e91479e8
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/pending_post_beacon-sendwithdata.tentative.https.window.js
@@ -0,0 +1,43 @@
+// META: script=/common/utils.js
+// META: script=./resources/pending_beacon-helper.js
+
+'use strict';
+
+// Test empty data.
+for (const dataType in BeaconDataType) {
+ postBeaconSendDataTest(
+ dataType, '', `Sent empty ${dataType}, and server got no data.`, {
+ expectNoData: true,
+ });
+}
+
+// Test small payload.
+for (const [dataType, skipCharset] of Object.entries(
+ BeaconDataTypeToSkipCharset)) {
+ postBeaconSendDataTest(
+ dataType, generateSequentialData(0, 1024, skipCharset),
+ 'Encoded and sent in POST request.');
+}
+
+// Test large payload.
+for (const dataType in BeaconDataType) {
+ postBeaconSendDataTest(
+ dataType, generatePayload(65536), 'Sent out big data.');
+}
+
+test(() => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ let beacon = new PendingPostBeacon(url);
+ assert_throws_js(TypeError, () => beacon.setData(new ReadableStream()));
+}, 'setData() does not support ReadableStream.');
+
+test(() => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ let beacon = new PendingPostBeacon(url);
+ const formData = new FormData();
+ formData.append('part1', 'value1');
+ formData.append('part2', new Blob(['value2']), 'file.txt');
+ assert_throws_js(RangeError, () => beacon.setData(formData));
+}, 'setData() does not support multi-parts data.');
diff --git a/testing/web-platform/tests/pending-beacon/resources/get_beacon.py b/testing/web-platform/tests/pending-beacon/resources/get_beacon.py
new file mode 100644
index 0000000000..32cb9a9ba3
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/resources/get_beacon.py
@@ -0,0 +1,30 @@
+"""An HTTP request handler for WPT that handles /get_beacon.py requests."""
+
+import json
+
+_BEACON_ID_KEY = b"uuid"
+_BEACON_DATA_PATH = "beacon_data"
+
+
+def main(request, response):
+ """Retrieves the beacon data keyed by the given uuid from server storage.
+
+ The response content is a JSON string in one of the following formats:
+ - "{'data': ['abc', null, '123',...]}"
+ - "{'data': []}" indicates that no data has been set for this uuid.
+ """
+ if _BEACON_ID_KEY not in request.GET:
+ response.status = 400
+ return "Must provide a UUID to store beacon data"
+ uuid = request.GET.first(_BEACON_ID_KEY)
+
+ with request.server.stash.lock:
+ body = {'data': []}
+ data = request.server.stash.take(key=uuid, path=_BEACON_DATA_PATH)
+ if data:
+ body['data'] = data
+ # The stash is read-once/write-once, so it has to be put back after
+ # reading if `data` is not None.
+ request.server.stash.put(
+ key=uuid, value=data, path=_BEACON_DATA_PATH)
+ return [(b'Content-Type', b'text/plain')], json.dumps(body)
diff --git a/testing/web-platform/tests/pending-beacon/resources/pending_beacon-helper.js b/testing/web-platform/tests/pending-beacon/resources/pending_beacon-helper.js
new file mode 100644
index 0000000000..e7b6ea5cb6
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/resources/pending_beacon-helper.js
@@ -0,0 +1,242 @@
+'use strict';
+
+const ROOT_NAME = 'pending-beacon';
+
+function parallelPromiseTest(func, description) {
+ async_test((t) => {
+ Promise.resolve(func(t)).then(() => t.done()).catch(t.step_func((e) => {
+ throw e;
+ }));
+ }, description);
+}
+
+const BeaconTypes = [
+ {type: PendingPostBeacon, name: 'PendingPostBeacon', expectedMethod: 'POST'},
+ {type: PendingGetBeacon, name: 'PendingGetBeacon', expectedMethod: 'GET'},
+];
+
+/** @enum {string} */
+const BeaconDataType = {
+ String: 'String',
+ ArrayBuffer: 'ArrayBuffer',
+ FormData: 'FormData',
+ URLSearchParams: 'URLSearchParams',
+ Blob: 'Blob',
+ File: 'File',
+};
+
+/** @enum {string} */
+const BeaconDataTypeToSkipCharset = {
+ String: '',
+ ArrayBuffer: '',
+ FormData: '\n\r', // CRLF characters will be normalized by FormData
+ URLSearchParams: ';,/?:@&=+$', // reserved URI characters
+ Blob: '',
+ File: '',
+};
+
+const BEACON_PAYLOAD_KEY = 'payload';
+
+// Creates beacon data of the given `dataType` from `data`.
+// @param {string} data - A string representation of the beacon data. Note that
+// it cannot contain UTF-16 surrogates for all `BeaconDataType` except BLOB.
+// @param {BeaconDataType} dataType - must be one of `BeaconDataType`.
+// @param {string} contentType - Request Content-Type.
+function makeBeaconData(data, dataType, contentType) {
+ switch (dataType) {
+ case BeaconDataType.String:
+ return data;
+ case BeaconDataType.ArrayBuffer:
+ return new TextEncoder().encode(data).buffer;
+ case BeaconDataType.FormData:
+ const formData = new FormData();
+ if (data.length > 0) {
+ formData.append(BEACON_PAYLOAD_KEY, data);
+ }
+ return formData;
+ case BeaconDataType.URLSearchParams:
+ if (data.length > 0) {
+ return new URLSearchParams(`${BEACON_PAYLOAD_KEY}=${data}`);
+ }
+ return new URLSearchParams();
+ case BeaconDataType.Blob: {
+ const options = {type: contentType || undefined};
+ return new Blob([data], options);
+ }
+ case BeaconDataType.File: {
+ const options = {type: contentType || 'text/plain'};
+ return new File([data], 'file.txt', options);
+ }
+ default:
+ throw Error(`Unsupported beacon dataType: ${dataType}`);
+ }
+}
+
+// Create a string of `end`-`begin` characters, with characters starting from
+// UTF-16 code unit `begin` to `end`-1.
+function generateSequentialData(begin, end, skip) {
+ const codeUnits = Array(end - begin).fill().map((el, i) => i + begin);
+ if (skip) {
+ return String.fromCharCode(
+ ...codeUnits.filter(c => !skip.includes(String.fromCharCode(c))));
+ }
+ return String.fromCharCode(...codeUnits);
+}
+
+function generatePayload(size) {
+ if (size == 0) {
+ return '';
+ }
+ const prefix = String(size) + ':';
+ if (size < prefix.length) {
+ return Array(size).fill('*').join('');
+ }
+ if (size == prefix.length) {
+ return prefix;
+ }
+
+ return prefix + Array(size - prefix.length).fill('*').join('');
+}
+
+function generateSetBeaconURL(uuid, options) {
+ const host = (options && options.host) || '';
+ let url = `${host}/${ROOT_NAME}/resources/set_beacon.py?uuid=${uuid}`;
+ if (options) {
+ if (options.expectOrigin !== undefined) {
+ url = `${url}&expectOrigin=${options.expectOrigin}`;
+ }
+ if (options.expectPreflight !== undefined) {
+ url = `${url}&expectPreflight=${options.expectPreflight}`;
+ }
+ if (options.expectCredentials !== undefined) {
+ url = `${url}&expectCredentials=${options.expectCredentials}`;
+ }
+
+ if (options.useRedirectHandler) {
+ const redirect = `${host}/common/redirect.py` +
+ `?location=${encodeURIComponent(url)}`;
+ url = redirect;
+ }
+ }
+ return url;
+}
+
+async function poll(asyncFunc, expected) {
+ const maxRetries = 30;
+ const waitInterval = 100; // milliseconds.
+ const delay = ms => new Promise(res => setTimeout(res, ms));
+
+ let result = {data: []};
+ for (let i = 0; i < maxRetries; i++) {
+ result = await asyncFunc();
+ if (!expected(result)) {
+ await delay(waitInterval);
+ continue;
+ }
+ return result;
+ }
+ return result;
+}
+
+// Waits until the `options.count` number of beacon data available from the
+// server. Defaults to 1.
+// If `options.data` is set, it will be used to compare with the data from the
+// response.
+async function expectBeacon(uuid, options) {
+ const expectedCount =
+ (options && options.count !== undefined) ? options.count : 1;
+
+ const res = await poll(
+ async () => {
+ const res = await fetch(
+ `/${ROOT_NAME}/resources/get_beacon.py?uuid=${uuid}`,
+ {cache: 'no-store'});
+ return await res.json();
+ },
+ (res) => {
+ if (expectedCount == 0) {
+ // If expecting no beacon, we should try to wait as long as possible.
+ // So always returning false here until `poll()` decides to terminate
+ // itself.
+ return false;
+ }
+ return res.data.length == expectedCount;
+ });
+ if (!options || !options.data) {
+ assert_equals(
+ res.data.length, expectedCount,
+ 'Number of sent beacons does not match expected count:');
+ return;
+ }
+
+ if (expectedCount == 0) {
+ assert_equals(
+ res.data.length, 0,
+ 'Number of sent beacons does not match expected count:');
+ return;
+ }
+
+ const decoder = options && options.percentDecoded ? (s) => {
+ // application/x-www-form-urlencoded serializer encodes space as '+'
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
+ s = s.replace(/\+/g, '%20');
+ return decodeURIComponent(s);
+ } : (s) => s;
+
+ assert_equals(
+ res.data.length, options.data.length,
+ `The size of beacon data ${
+ res.data.length} from server does not match expected value ${
+ options.data.length}.`);
+ for (let i = 0; i < options.data.length; i++) {
+ assert_equals(
+ decoder(res.data[i]), options.data[i],
+ 'The beacon data does not match expected value.');
+ }
+}
+
+function postBeaconSendDataTest(dataType, testData, description, options) {
+ parallelPromiseTest(async t => {
+ const expectNoData = options && options.expectNoData;
+ const expectCount = (options && options.expectCount !== undefined) ?
+ options.expectCount :
+ 1;
+ const uuid = token();
+ const url =
+ generateSetBeaconURL(uuid, (options && options.urlOptions) || {});
+ const beacon = new PendingPostBeacon(url);
+ assert_equals(beacon.method, 'POST', 'must be POST to call setData().');
+
+ if (options && options.setCookie) {
+ document.cookie = options.setCookie;
+ }
+
+ beacon.setData(makeBeaconData(
+ testData, dataType, (options && options.contentType) || {}));
+ beacon.sendNow();
+
+ const expectedData = expectNoData ? null : testData;
+ const percentDecoded =
+ !expectNoData && dataType === BeaconDataType.URLSearchParams;
+ await expectBeacon(uuid, {
+ count: expectCount,
+ data: [expectedData],
+ percentDecoded: percentDecoded
+ });
+ }, `PendingPostBeacon(${dataType}): ${description}`);
+}
+
+function generateHTML(script) {
+ return `<!DOCTYPE html><body><script>${script}</script></body>`;
+}
+
+// Loads `script` into an iframe and appends it to the current document.
+// Returns the loaded iframe element.
+async function loadScriptAsIframe(script) {
+ const iframe = document.createElement('iframe');
+ iframe.srcdoc = generateHTML(script);
+ const iframeLoaded = new Promise(resolve => iframe.onload = resolve);
+ document.body.appendChild(iframe);
+ await iframeLoaded;
+ return iframe;
+}
diff --git a/testing/web-platform/tests/pending-beacon/resources/set_beacon.py b/testing/web-platform/tests/pending-beacon/resources/set_beacon.py
new file mode 100644
index 0000000000..1c71f23e57
--- /dev/null
+++ b/testing/web-platform/tests/pending-beacon/resources/set_beacon.py
@@ -0,0 +1,83 @@
+"""An HTTP request handler for WPT that handles /set_beacon.py requests."""
+
+_BEACON_ID_KEY = b"uuid"
+_BEACON_DATA_PATH = "beacon_data"
+_BEACON_FORM_PAYLOAD_KEY = b"payload"
+_BEACON_BODY_PAYLOAD_KEY = "payload="
+_BEACON_EXPECT_ORIGIN_KEY = b"expectOrigin"
+_BEACON_EXPECT_PREFLIGHT_KEY = b"expectPreflight"
+_BEACON_EXPECT_CREDS_KEY = b"expectCredentials"
+
+
+def main(request, response):
+ """Stores the given beacon's data keyed by uuid in the server.
+
+ For GET request, this handler assumes no data.
+ For POST request, this handler extracts data from request body:
+ - Content-Type=multipart/form-data: data keyed by 'payload'.
+ - the entire request body.
+
+ Multiple data can be added for the same uuid.
+
+ The data is stored as UTF-8 format.
+ """
+ if _BEACON_ID_KEY not in request.GET:
+ response.status = 400
+ return "Must provide a UUID to store beacon data"
+ uuid = request.GET.first(_BEACON_ID_KEY)
+
+ expected_origin = request.GET.get(_BEACON_EXPECT_ORIGIN_KEY)
+ if b"origin" in request.headers:
+ origin = request.headers.get(b"origin")
+ if expected_origin:
+ assert origin == expected_origin, f"expected {expected_origin}, got {origin}"
+ response.headers.set(b"Access-Control-Allow-Origin", origin)
+ else:
+ assert expected_origin is None, f"expected None, got {expected_origin}"
+
+ # Handles preflight request first.
+ if request.method == u"OPTIONS":
+ assert request.GET.get(
+ _BEACON_EXPECT_PREFLIGHT_KEY) == b"true", "Preflight not expected."
+
+ # preflight must not have cookies.
+ assert b"Cookie" not in request.headers
+
+ requested_headers = request.headers.get(
+ b"Access-Control-Request-Headers")
+ assert b"content-type" in requested_headers, f"expected content-type, got {requested_headers}"
+ response.headers.set(b"Access-Control-Allow-Headers", b"content-type")
+
+ requested_method = request.headers.get(b"Access-Control-Request-Method")
+ assert requested_method == b"POST", f"expected POST, got {requested_method}"
+ response.headers.set(b"Access-Control-Allow-Methods", b"POST")
+
+ return response
+
+ expect_creds = request.GET.get(_BEACON_EXPECT_CREDS_KEY) == b"true"
+ if expect_creds:
+ assert b"Cookie" in request.headers
+ else:
+ assert b"Cookie" not in request.headers
+
+ data = None
+ if request.method == u"POST":
+ if b"multipart/form-data" in request.headers.get(b"Content-Type", b""):
+ if _BEACON_FORM_PAYLOAD_KEY in request.POST:
+ data = request.POST.first(_BEACON_FORM_PAYLOAD_KEY).decode(
+ 'utf-8')
+ elif request.body:
+ data = request.body.decode('utf-8')
+ if data.startswith(_BEACON_BODY_PAYLOAD_KEY):
+ data = data.split(_BEACON_BODY_PAYLOAD_KEY)[1]
+
+ with request.server.stash.lock:
+ saved_data = request.server.stash.take(key=uuid, path=_BEACON_DATA_PATH)
+ if not saved_data:
+ saved_data = [data]
+ else:
+ saved_data.append(data)
+ request.server.stash.put(
+ key=uuid, value=saved_data, path=_BEACON_DATA_PATH)
+
+ response.status = 200