diff --git a/test/common/wpt.js b/test/common/wpt.js
index 584f3c177ab0be..b865908c70b11c 100644
--- a/test/common/wpt.js
+++ b/test/common/wpt.js
@@ -624,6 +624,7 @@ class WPTRunner {
switch (name) {
case 'Window': {
this.globalThisInitScripts.push('globalThis.Window = Object.getPrototypeOf(globalThis).constructor;');
+ this.globalThisInitScripts.push('globalThis.GLOBAL.isWindow = () => true;');
break;
}
diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js
index 855ec7e91c394b..71e68f8a0e2536 100644
--- a/test/common/wpt/worker.js
+++ b/test/common/wpt/worker.js
@@ -16,6 +16,7 @@ if (workerData.needsGc) {
globalThis.self = global;
globalThis.GLOBAL = {
isWindow() { return false; },
+ isWorker() { return false; },
isShadowRealm() { return false; },
};
globalThis.require = require;
diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index 2ce7de20ded95f..e79a8479b4ee3a 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -16,6 +16,7 @@ Last update:
- dom/abort: https://github.com/web-platform-tests/wpt/tree/dc928169ee/dom/abort
- dom/events: https://github.com/web-platform-tests/wpt/tree/0a811c5161/dom/events
- encoding: https://github.com/web-platform-tests/wpt/tree/1ac8deee08/encoding
+- fetch/api: https://github.com/web-platform-tests/wpt/tree/75b487b9ed/fetch/api
- fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources
- FileAPI: https://github.com/web-platform-tests/wpt/tree/7f51301888/FileAPI
- hr-time: https://github.com/web-platform-tests/wpt/tree/34cafd797e/hr-time
@@ -23,7 +24,7 @@ Last update:
- html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing
- html/webappapis/structured-clone: https://github.com/web-platform-tests/wpt/tree/47d3fb280c/html/webappapis/structured-clone
- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/5873f2d8f1/html/webappapis/timers
-- interfaces: https://github.com/web-platform-tests/wpt/tree/e1b27be06b/interfaces
+- interfaces: https://github.com/web-platform-tests/wpt/tree/b619cb7f23/interfaces
- performance-timeline: https://github.com/web-platform-tests/wpt/tree/94caab7038/performance-timeline
- resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
- resources: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/resources
diff --git a/test/fixtures/wpt/fetch/api/abort/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/abort/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..e926c1406232f9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: abortable-fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/abort/cache.https.any.js b/test/fixtures/wpt/fetch/api/abort/cache.https.any.js
new file mode 100644
index 00000000000000..bdaf0e69e58010
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/cache.https.any.js
@@ -0,0 +1,47 @@
+// META: title=Request signals & the cache API
+// META: global=window,worker
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ controller.abort();
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API");
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+ controller.abort();
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API, even if they're already aborted");
diff --git a/test/fixtures/wpt/fetch/api/abort/destroyed-context.html b/test/fixtures/wpt/fetch/api/abort/destroyed-context.html
new file mode 100644
index 00000000000000..161d39bd9ce3db
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/destroyed-context.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/abort/general.any.js b/test/fixtures/wpt/fetch/api/abort/general.any.js
new file mode 100644
index 00000000000000..139f08947b15ac
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/general.any.js
@@ -0,0 +1,572 @@
+// META: timeout=long
+// META: global=window,worker
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../request/request-error.js
+
+const BODY_METHODS = ['arrayBuffer', 'blob', 'bytes', 'formData', 'json', 'text'];
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+// This is used to close connections that weren't correctly closed during the tests,
+// otherwise you can end up running out of HTTP connections.
+let requestAbortKeys = [];
+
+function abortRequests() {
+ const keys = requestAbortKeys;
+ requestAbortKeys = [];
+ return Promise.all(
+ keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`))
+ );
+}
+
+const hostInfo = get_host_info();
+const urlHostname = hostInfo.REMOTE_HOST;
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason');
+}, "Aborting rejects with abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const url = new URL('../resources/data.json', location);
+ url.hostname = urlHostname;
+
+ const fetchPromise = fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError - no-cors");
+
+// Test that errors thrown from the request constructor take priority over abort errors.
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ promise_test(async t => {
+ try {
+ // If this doesn't throw, we'll effectively skip the test.
+ // It'll fail properly in ../request/request-error.html
+ new Request(...args);
+ }
+ catch (err) {
+ const controller = new AbortController();
+ controller.abort();
+
+ // Add signal to 2nd arg
+ args[1] = args[1] || {};
+ args[1].signal = controller.signal;
+ await promise_rejects_js(t, TypeError, fetch(...args));
+ }
+ }, `TypeError from request constructor takes priority - ${testName}`);
+}
+
+test(() => {
+ const request = new Request('');
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+}, "Request objects have a signal property");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+ assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason");
+}, "Signal on request object should also have abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+ const requestFromRequest = new Request(request);
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json');
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal: new AbortController().signal });
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request overriding another");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const fetchPromise = fetch(request, {method: 'POST'});
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal retained after unrelated properties are overridden by fetch");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const data = await fetch(request, { signal: null }).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signal removed by setting to null");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const log = [];
+
+ await Promise.all([
+ fetch('../resources/data.json', { signal }).then(
+ () => assert_unreached("Fetch must not resolve"),
+ () => log.push('fetch-reject')
+ ),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ assert_array_equals(log, ['fetch-reject', 'next-microtask']);
+}, "Already aborted signal rejects immediately");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', {
+ signal,
+ method: 'POST',
+ body: 'foo',
+ headers: { 'Content-Type': 'text/plain' }
+ });
+
+ await fetch(request).catch(() => {});
+
+ assert_true(request.bodyUsed, "Body has been used");
+}, "Request is still 'used' if signal is aborted before fetching");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await fetch('../resources/data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+}
+
+promise_test(async (t) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const res = await fetch('../resources/data.json', { signal });
+ controller.abort();
+
+ await promise_rejects_dom(t, 'AbortError', res.text());
+ await promise_rejects_dom(t, 'AbortError', res.text());
+}, 'Call text() twice on aborted response');
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+ controller.abort();
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {});
+
+ // I'm hoping this will give the browser enough time to (incorrectly) make the request
+ // above, if it intends to.
+ await fetch('../resources/data.json').then(r => r.json());
+
+ const response = await fetch(`../resources/stash-take.py?key=${stateKey}`);
+ const data = await response.json();
+
+ assert_equals(data, null, "Request hasn't been made to the server");
+}, "Already aborted signal does not make request");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Already aborted signal can be used for many fetches");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch('../resources/data.json', { signal }).then(r => r.json());
+
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location);
+ url.hostname = urlHostname;
+
+ await fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location);
+ stashTakeURL.hostname = urlHostname;
+
+ const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response - no-cors");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ const bodyPromise = response[bodyMethod]();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+ }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`);
+}
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ await reader.read();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted, after reading. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await fetch(`../resources/empty.txt`, { signal });
+
+ // Read whole response to ensure close signal has sent.
+ await response.clone().text();
+
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ const item = await reader.read();
+
+ assert_true(item.done, "Stream is done");
+}, "Stream will not error if body is empty. It's closed with an empty queue before it errors.");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ let cancelReason;
+
+ const body = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(new Uint8Array([42]));
+ },
+ cancel(reason) {
+ cancelReason = reason;
+ }
+ });
+
+ const fetchPromise = fetch('../resources/empty.txt', {
+ body, signal,
+ method: 'POST',
+ duplex: 'half',
+ headers: {
+ 'Content-Type': 'text/plain'
+ }
+ });
+
+ assert_true(!!cancelReason, 'Cancel called sync');
+ assert_equals(cancelReason.constructor, DOMException);
+ assert_equals(cancelReason.name, 'AbortError');
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+
+ const fetchErr = await fetchPromise.catch(e => e);
+
+ assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance");
+}, "Readable stream synchronously cancels with AbortError if aborted before reading");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('.', { signal });
+ const requestSignal = request.signal;
+
+ const clonedRequest = request.clone();
+
+ assert_equals(requestSignal, request.signal, "Original request signal the same after cloning");
+ assert_true(request.signal.aborted, "Original request signal aborted");
+ assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal");
+ assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted");
+}, "Signal state is cloned");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const request = new Request('.', { signal });
+ const clonedRequest = request.clone();
+
+ const log = [];
+
+ request.signal.addEventListener('abort', () => log.push('original-aborted'));
+ clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted'));
+
+ controller.abort();
+
+ assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order");
+ assert_true(request.signal.aborted, 'Signal aborted');
+ assert_true(clonedRequest.signal.aborted, 'Signal aborted');
+}, "Clone aborts with original controller");
diff --git a/test/fixtures/wpt/fetch/api/abort/keepalive.html b/test/fixtures/wpt/fetch/api/abort/keepalive.html
new file mode 100644
index 00000000000000..db12df0d289be9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/keepalive.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/abort/request.any.js b/test/fixtures/wpt/fetch/api/abort/request.any.js
new file mode 100644
index 00000000000000..dcc7803abe5576
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/request.any.js
@@ -0,0 +1,85 @@
+// META: timeout=long
+// META: global=window,worker
+
+const BODY_FUNCTION_AND_DATA = {
+ arrayBuffer: null,
+ blob: null,
+ formData: new FormData(),
+ json: new Blob(["{}"]),
+ text: null,
+};
+
+for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Calling ${bodyFunction}() on an aborted request`);
+
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ const p = request[bodyFunction]();
+ controller.abort();
+ await p;
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Aborting a request after calling ${bodyFunction}()`);
+
+ if (!body) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted consumed request should still be able to run ${bodyFunction}() when empty`
+ );
+ }, `Calling ${bodyFunction}() on an aborted consumed empty request`);
+ }
+
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body: body || new Blob(["foo"]),
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await promise_rejects_js(t, TypeError, request[bodyFunction]());
+ }, `Calling ${bodyFunction}() on an aborted consumed nonempty request`);
+}
diff --git a/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html b/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html
new file mode 100644
index 00000000000000..1867e205bb6ee1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html
@@ -0,0 +1,212 @@
+
+
+
+
+ Aborting fetch when intercepted by a service worker
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..9369edb817efe1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml
@@ -0,0 +1,21 @@
+features:
+- name: fetch
+ files:
+ - "*"
+ - "!request-upload*"
+ - "!request-private-network-headers.tentative.any.js"
+- name: fetch-request-streams
+ files:
+ - request-upload*
+- name: private-network-access
+ files:
+ - request-private-network-headers.tentative.any.js
+- name: early-data
+ files:
+ # Note: Test coverage for this feature is very incomplete
+ - http-response-code.any.js
+# The following classifier for "http2" intentionally overlaps with the above
+# classifier for "fetch" because "statusText" is a Fetch API concept.
+- name: http2
+ files:
+ - status.h2.any.js
diff --git a/test/fixtures/wpt/fetch/api/basic/accept-header.any.js b/test/fixtures/wpt/fetch/api/basic/accept-header.any.js
new file mode 100644
index 00000000000000..cd54cf2a03e8a9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/accept-header.any.js
@@ -0,0 +1,34 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'");
+ });
+}, "Request through fetch should have 'accept' header with value '*/*'");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'");
+ });
+}, "Request through fetch should have 'accept' header with value 'custom/*'");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_true(response.headers.has("x-request-accept-language"));
+ });
+}, "Request through fetch should have a 'accept-language' header");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'");
+ });
+}, "Request through fetch should have 'accept-language' header with value 'bzh'");
diff --git a/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html b/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html
new file mode 100644
index 00000000000000..afc2bbbafb0942
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html
@@ -0,0 +1,43 @@
+
+
+Block mime type as script
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js b/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js
new file mode 100644
index 00000000000000..2f9fa81c02b18b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js
@@ -0,0 +1,38 @@
+// META: title=Request ETag
+// META: global=window,worker
+// META: script=/common/utils.js
+
+promise_test(function() {
+ var cacheBuster = token(); // ensures first request is uncached
+ var url = "../resources/cache.py?v=" + cacheBuster;
+ var etag;
+
+ // make the first request
+ return fetch(url).then(function(response) {
+ // ensure we're getting the regular, uncached response
+ assert_equals(response.status, 200);
+ assert_equals(response.headers.get("X-HTTP-STATUS"), null)
+
+ return response.text(); // consuming the body, just to be safe
+ }).then(function(body) {
+ // make a second request
+ return fetch(url);
+ }).then(function(response) {
+ // while the server responds with 304 if our browser sent the correct
+ // If-None-Match request header, at the JavaScript level this surfaces
+ // as 200
+ assert_equals(response.status, 200);
+ assert_equals(response.headers.get("X-HTTP-STATUS"), "304")
+
+ etag = response.headers.get("ETag")
+
+ return response.text(); // consuming the body, just to be safe
+ }).then(function(body) {
+ // make a third request, explicitly setting If-None-Match request header
+ var headers = { "If-None-Match": etag }
+ return fetch(url, { headers: headers })
+ }).then(function(response) {
+ // 304 now surfaces thanks to the explicit If-None-Match request header
+ assert_equals(response.status, 304);
+ });
+}, "Testing conditional GET with ETags");
diff --git a/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js b/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js
new file mode 100644
index 00000000000000..f7114425f95504
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js
@@ -0,0 +1,24 @@
+// META: title=Fetch: network timeout after receiving the HTTP response headers
+// META: global=window,worker
+// META: timeout=long
+// META: script=../resources/utils.js
+
+function checkReader(test, reader, promiseToTest)
+{
+ return reader.read().then((value) => {
+ validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk");
+ return promise_rejects_js(test, TypeError, promiseToTest(reader));
+ });
+}
+
+promise_test((test) => {
+ return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
+ return checkReader(test, response.body.getReader(), reader => reader.read());
+ });
+}, "Response reader read() promise should reject after a network error happening after resolving fetch promise");
+
+promise_test((test) => {
+ return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
+ return checkReader(test, response.body.getReader(), reader => reader.closed);
+ });
+}, "Response reader closed promise should reject after a network error happening after resolving fetch promise");
diff --git a/test/fixtures/wpt/fetch/api/basic/gc.any.js b/test/fixtures/wpt/fetch/api/basic/gc.any.js
new file mode 100644
index 00000000000000..70362ff39ce7d5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/gc.any.js
@@ -0,0 +1,19 @@
+// META: global=window,worker
+// META: script=/common/gc.js
+
+promise_test(async () => {
+ let i = 0;
+ const repeat = 5;
+ const buffer = await new Response(new ReadableStream({
+ pull(c) {
+ if (i >= repeat) {
+ c.close();
+ return;
+ }
+ ++i;
+ c.enqueue(new Uint8Array([0]))
+ garbageCollect();
+ }
+ })).arrayBuffer();
+ assert_equals(buffer.byteLength, repeat, `The buffer should be ${repeat}-byte long`);
+}, "GC/CC should not abruptly close the stream while being consumed by Response");
diff --git a/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js b/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js
new file mode 100644
index 00000000000000..bb70d87d250cda
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js
@@ -0,0 +1,15 @@
+// META: global=window,worker
+
+[
+ ["content-length", "0", "header-content-length"],
+ ["content-length", "0, 0", "header-content-length-twice"],
+ ["double-trouble", ", ", "headers-double-empty"],
+ ["foo-test", "1, 2, 3", "headers-basic"],
+ ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"],
+ ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"],
+].forEach(testValues => {
+ promise_test(async t => {
+ const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis");
+ assert_equals(response.headers.get(testValues[0]), testValues[1]);
+ }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]);
+});
diff --git a/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js b/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js
new file mode 100644
index 00000000000000..741d83bf7aaa55
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js
@@ -0,0 +1,5 @@
+// META: global=window,worker
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x")));
+}, "Ensure fetch() rejects null bytes in headers");
diff --git a/test/fixtures/wpt/fetch/api/basic/historical.any.js b/test/fixtures/wpt/fetch/api/basic/historical.any.js
new file mode 100644
index 00000000000000..c8081262168e36
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/historical.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+test(() => {
+ assert_false("getAll" in new Headers());
+ assert_false("getAll" in Headers.prototype);
+}, "Headers object no longer has a getAll() method");
+
+test(() => {
+ assert_false("type" in new Request("about:blank"));
+ assert_false("type" in Request.prototype);
+}, "'type' getter should not exist on Request objects");
+
+// See https://github.com/whatwg/fetch/pull/979 for the removal
+test(() => {
+ assert_false("trailer" in new Response());
+ assert_false("trailer" in Response.prototype);
+}, "Response object no longer has a trailer getter");
diff --git a/test/fixtures/wpt/fetch/api/basic/http-response-code.any.js b/test/fixtures/wpt/fetch/api/basic/http-response-code.any.js
new file mode 100644
index 00000000000000..1fd312a3e9fa61
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/http-response-code.any.js
@@ -0,0 +1,14 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+promise_test(async (test) => {
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=425&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
+ + `&dispatch=check_partition&addcounter=true`);
+ assert_equals(resp.status, 425);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 1 times. 1 connections were created.");
+}, "Fetch on 425 response should not be retried for non TLS early data.");
diff --git a/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js b/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js
new file mode 100644
index 00000000000000..e3cfd1b2f6e666
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js
@@ -0,0 +1,87 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: script=../resources/utils.js
+
+function integrity(desc, url, integrity, initRequestMode, shouldPass) {
+ var fetchRequestInit = {'integrity': integrity}
+ if (!!initRequestMode && initRequestMode !== "") {
+ fetchRequestInit.mode = initRequestMode;
+ }
+
+ if (shouldPass) {
+ promise_test(function(test) {
+ return fetch(url, fetchRequestInit).then(function(resp) {
+ if (initRequestMode !== "no-cors") {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ } else {
+ assert_equals(resp.status, 0, "Opaque response's status is 0");
+ assert_equals(resp.type, "opaque");
+ }
+ });
+ }, desc);
+ } else {
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit));
+ }, desc);
+ }
+}
+
+const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk=";
+const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL";
+const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const topSha512wrongpadding = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg";
+const topSha512base64url = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const topSha512base64url_nopadding = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg";
+const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I=";
+const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg==";
+
+const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+const url = path;
+const corsUrl =
+ `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`;
+const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}`
+
+integrity("Empty string integrity", url, "", /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-512 integrity with missing padding", url, topSha512wrongpadding,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("SHA-512 integrity base64url encoded", url, topSha512base64url,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("SHA-512 integrity base64url encoded with missing padding", url,
+ topSha512base64url_nopadding, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Invalid integrity", url, invalidSha256,
+ /* initRequestMode */ undefined, /* shouldPass */ false);
+integrity("Multiple integrities: valid stronger than invalid", url,
+ invalidSha256 + " " + topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: invalid stronger than valid",
+ url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ false);
+integrity("Multiple integrities: invalid as strong as valid", url,
+ invalidSha512 + " " + topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: both are valid", url,
+ topSha384 + " " + topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: both are invalid", url,
+ invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ false);
+integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("CORS SHA-512 integrity", corsUrl, topSha512,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("CORS invalid integrity", corsUrl, invalidSha512,
+ /* initRequestMode */ undefined, /* shouldPass */ false);
+
+integrity("Empty string integrity for opaque response", corsUrl2, "",
+ /* initRequestMode */ "no-cors", /* shouldPass */ true);
+integrity("SHA-* integrity for opaque response", corsUrl2, topSha512,
+ /* initRequestMode */ "no-cors", /* shouldPass */ false);
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/basic/keepalive.any.js b/test/fixtures/wpt/fetch/api/basic/keepalive.any.js
new file mode 100644
index 00000000000000..d4e831b963101e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/keepalive.any.js
@@ -0,0 +1,77 @@
+// META: global=window
+// META: timeout=long
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+/**
+ * In a different-site iframe, test to fetch a keepalive URL on the specified
+ * document event.
+ */
+function keepaliveSimpleRequestTest(method) {
+ for (const evt of ['load', 'unload', 'pagehide']) {
+ const desc =
+ `[keepalive] simple ${method} request on '${evt}' [no payload]`;
+ promise_test(async (test) => {
+ const token1 = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveIframeUrl(token1, method, {sendOn: evt});
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ if (evt != 'load') {
+ iframe.remove();
+ }
+
+ assertStashedTokenAsync(desc, token1);
+ }, `${desc}; setting up`);
+ }
+}
+
+for (const method of ['GET', 'POST']) {
+ keepaliveSimpleRequestTest(method);
+}
+
+// verifies fetch keepalive requests from a worker
+function keepaliveSimpleWorkerTest() {
+ const desc =
+ `simple keepalive test for web workers`;
+ promise_test(async (test) => {
+ const TOKEN = token();
+ const FRAME_ORIGIN = new URL(location.href).origin;
+ const TEST_URL = get_host_info().HTTP_ORIGIN + `/fetch/api/resources/stash-put.py?key=${TOKEN}&value=on`
+ + `&frame_origin=${FRAME_ORIGIN}`;
+ // start a worker which sends keepalive request and immediately terminates
+ const worker = new Worker(`/fetch/api/resources/keepalive-worker.js?param=${TEST_URL}`);
+
+ const keepAliveWorkerPromise = new Promise((resolve, reject) => {
+ worker.onmessage = (event) => {
+ if (event.data === 'started') {
+ resolve();
+ } else {
+ reject(new Error("Unexpected message received from worker"));
+ }
+ };
+ worker.onerror = (error) => {
+ reject(error);
+ };
+ });
+
+ // wait until the worker has been initialized (indicated by the "started" message)
+ await keepAliveWorkerPromise;
+ // verifies if the token sent in fetch request has been updated in the server
+ assertStashedTokenAsync(desc, TOKEN);
+
+ }, `${desc};`);
+
+}
+
+keepaliveSimpleWorkerTest();
diff --git a/test/fixtures/wpt/fetch/api/basic/mediasource.window.js b/test/fixtures/wpt/fetch/api/basic/mediasource.window.js
new file mode 100644
index 00000000000000..1f89595393da41
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/mediasource.window.js
@@ -0,0 +1,5 @@
+promise_test(t => {
+ const mediaSource = new MediaSource(),
+ mediaSourceURL = URL.createObjectURL(mediaSource);
+ return promise_rejects_js(t, TypeError, fetch(mediaSourceURL));
+}, "Cannot fetch blob: URL from a MediaSource");
diff --git a/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js b/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js
new file mode 100644
index 00000000000000..a4abcac55f39a9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js
@@ -0,0 +1,29 @@
+// META: script=../resources/utils.js
+
+function fetchNoCors(url, isOpaqueFiltered) {
+ var urlQuery = "?pipe=header(x-is-filtered,value)"
+ promise_test(function(test) {
+ if (isOpaqueFiltered)
+ return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+ assert_equals(resp.status, 0, "Opaque filter: status is 0");
+ assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+ assert_equals(resp.url, "", "Opaque filter: url is \"\"");
+ assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+ assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered");
+ });
+ else
+ return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered");
+ });
+ }, "Fetch "+ url + " with no-cors mode");
+}
+
+fetchNoCors(RESOURCES_DIR + "top.txt", false);
+fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false);
+fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true);
+fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true);
+
+done();
+
diff --git a/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js b/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js
new file mode 100644
index 00000000000000..1457702f1b163b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js
@@ -0,0 +1,28 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function fetchSameOrigin(url, shouldPass) {
+ promise_test(function(test) {
+ if (shouldPass)
+ return fetch(url , {"mode": "same-origin"}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ });
+ else
+ return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"}));
+ }, "Fetch "+ url + " with same-origin mode");
+}
+
+var host_info = get_host_info();
+
+fetchSameOrigin(RESOURCES_DIR + "top.txt", true);
+fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true);
+fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false);
+fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false);
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location=";
+
+fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true);
+fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true);
+fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false);
+fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false);
diff --git a/test/fixtures/wpt/fetch/api/basic/referrer.any.js b/test/fixtures/wpt/fetch/api/basic/referrer.any.js
new file mode 100644
index 00000000000000..85745e692a2fe0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/referrer.any.js
@@ -0,0 +1,29 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function runTest(url, init, expectedReferrer, title) {
+ promise_test(function(test) {
+ url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors";
+
+ return fetch(url , init).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct");
+ });
+ }, title);
+}
+
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py";
+var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py";
+var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ;
+var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location=";
+
+runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL");
+runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL");
+runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection");
+runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection");
+
+
+var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@");
+runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped");
+var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier";
+runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped");
diff --git a/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js b/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js
new file mode 100644
index 00000000000000..d7560f03a23e6c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js
@@ -0,0 +1,82 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function requestValidOverrideHeaders(desc, validHeaders) {
+ var url = RESOURCES_DIR + "inspect-headers.py";
+ var requestInit = {"headers": validHeaders}
+ var urlParameters = "?headers=" + Object.keys(validHeaders).join("|");
+
+ promise_test(function(test){
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ for (var header in validHeaders)
+ assert_equals(resp.headers.get("x-request-" + header), validHeaders[header], header + "is not skipped for non-forbidden methods");
+ });
+ }, desc);
+}
+
+requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"});
+requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""});
+
+requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""});
+requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""});
+requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"});
+requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"});
+requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"});
+requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"});
+requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"});
+requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"});
+requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"});
+requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"});
+requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"});
+requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"});
+requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"});
+requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"});
+requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"});
+requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"});
+requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"});
+requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"});
+requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"});
+requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"});
+requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"});
+requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"});
+
+let forbiddenMethods = [
+ "TRACE",
+ "TRACK",
+ "CONNECT",
+ "trace",
+ "track",
+ "connect",
+ "trace,",
+ "GET,track ",
+ " connect",
+];
+
+let overrideHeaders = [
+ "x-http-method-override",
+ "x-http-method",
+ "x-method-override",
+ "X-HTTP-METHOD-OVERRIDE",
+ "X-HTTP-METHOD",
+ "X-METHOD-OVERRIDE",
+];
+
+for (forbiddenMethod of forbiddenMethods) {
+ for (overrideHeader of overrideHeaders) {
+ requestForbiddenHeaders(`header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`, {[overrideHeader]: forbiddenMethod});
+ }
+}
+
+let permittedValues = [
+ "GETTRACE",
+ "GET",
+ "\",TRACE\",",
+];
+
+for (permittedValue of permittedValues) {
+ for (overrideHeader of overrideHeaders) {
+ requestValidOverrideHeaders(`header ${overrideHeader} is allowed to use value ${permittedValue}`, {[overrideHeader]: permittedValue});
+ }
+}
diff --git a/test/fixtures/wpt/fetch/api/basic/request-head.any.js b/test/fixtures/wpt/fetch/api/basic/request-head.any.js
new file mode 100644
index 00000000000000..e0b6afa079a400
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-head.any.js
@@ -0,0 +1,6 @@
+// META: global=window,worker
+
+promise_test(function(test) {
+ var requestInit = {"method": "HEAD", "body": "test"};
+ return promise_rejects_js(test, TypeError, fetch(".", requestInit));
+}, "Fetch with HEAD with body");
diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js
new file mode 100644
index 00000000000000..4c10e717f8c2e5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js
@@ -0,0 +1,13 @@
+// META: global=window,worker
+
+promise_test(() => {
+ return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => {
+ assert_regexp_match(body, /THIS-is-A-test: 1, 2/)
+ })
+}, "Multiple headers with the same name, different case (THIS-is-A-test first)")
+
+promise_test(() => {
+ return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => {
+ assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/)
+ })
+}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)")
diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js
new file mode 100644
index 00000000000000..4a9a8011385351
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+
+// This tests characters that are not
+// https://infra.spec.whatwg.org/#ascii-code-point
+// but are still
+// https://infra.spec.whatwg.org/#byte-value
+// in request header values.
+// Such request header values are valid and thus sent to servers.
+// Characters outside the #byte-value range are tested e.g. in
+// fetch/api/headers/headers-errors.html.
+
+promise_test(() => {
+ return fetch(
+ "../resources/inspect-headers.py?headers=accept|x-test",
+ {headers: {
+ "Accept": "before-æøå-after",
+ "X-Test": "before-ß-after"
+ }})
+ .then(res => {
+ assert_equals(
+ res.headers.get("x-request-accept"),
+ "before-æøå-after",
+ "Accept Header");
+ assert_equals(
+ res.headers.get("x-request-x-test"),
+ "before-ß-after",
+ "X-Test Header");
+ });
+}, "Non-ascii bytes in request headers");
diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers.any.js
new file mode 100644
index 00000000000000..f6a7fe1494bb61
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-headers.any.js
@@ -0,0 +1,83 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkContentType(contentType, body)
+{
+ if (self.FormData && body instanceof self.FormData) {
+ assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType);
+ return;
+ }
+
+ var expectedContentType = "text/plain;charset=UTF-8";
+ if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer)
+ expectedContentType = null;
+ else if (body instanceof Blob)
+ expectedContentType = body.type ? body.type : null;
+ else if (body instanceof URLSearchParams)
+ expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8";
+
+ assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType);
+}
+
+function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) {
+ var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type";
+ var requestInit = {"method": method}
+ promise_test(function(test){
+ if (typeof body === "function")
+ body = body();
+ if (body)
+ requestInit["body"] = body;
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent");
+ assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset");
+ assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin);
+ if (expectedContentLength !== undefined)
+ assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength);
+ checkContentType(resp.headers.get("x-request-content-type"), body);
+ });
+ }, desc);
+}
+
+var url = RESOURCES_DIR + "inspect-headers.py"
+
+requestHeaders("Fetch with GET", url, "GET", null, null, null);
+requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null);
+requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0");
+requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14");
+requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0");
+requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14");
+requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin);
+requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10");
+requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4");
+requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4");
+requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Float16Array body", url, "POST", () => new Float16Array(1), location.origin, "2");
+requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4");
+requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8");
+requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4");
+requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4");
+requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null);
+requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14");
+
+function requestOriginHeader(method, mode, needsOrigin) {
+ promise_test(function(test){
+ return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ if(needsOrigin)
+ assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin);
+ else
+ assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header")
+ });
+ }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header");
+}
+
+requestOriginHeader("GET", "cors", false);
+requestOriginHeader("POST", "same-origin", true);
+requestOriginHeader("POST", "no-cors", true);
+requestOriginHeader("PUT", "same-origin", true);
+requestOriginHeader("TacO", "same-origin", true);
+requestOriginHeader("TacO", "cors", true);
diff --git a/test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js b/test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js
new file mode 100644
index 00000000000000..9662a91c1770ad
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js
@@ -0,0 +1,18 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+requestForbiddenHeaders(
+ 'Access-Control-Request-Private-Network is a forbidden request header',
+ {'Access-Control-Request-Private-Network': ''});
+
+var invalidRequestHeaders = [
+ ["Access-Control-Request-Private-Network", "KO"],
+];
+
+invalidRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), null);
+ }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\"");
+});
diff --git a/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html b/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html
new file mode 100644
index 00000000000000..bdea1e185314aa
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Fetch in worker: referrer header
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js b/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js
new file mode 100644
index 00000000000000..0c3357642d674b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function testReferrer(referrer, expected, desc) {
+ promise_test(function(test) {
+ var url = RESOURCES_DIR + "inspect-headers.py?headers=referer"
+ var req = new Request(url, { referrer: referrer });
+ return fetch(req).then(function(resp) {
+ var actual = resp.headers.get("x-request-referer");
+ if (expected) {
+ assert_equals(actual, expected, "request's referer should be: " + expected);
+ return;
+ }
+ if (actual) {
+ assert_equals(actual, "", "request's referer should be empty");
+ }
+ });
+ }, desc);
+}
+
+testReferrer("about:client", self.location.href, 'about:client referrer');
+
+var fooURL = new URL("./foo", self.location).href;
+testReferrer(fooURL, fooURL, 'url referrer');
diff --git a/test/fixtures/wpt/fetch/api/basic/request-upload.any.js b/test/fixtures/wpt/fetch/api/basic/request-upload.any.js
new file mode 100644
index 00000000000000..0c4813bb5317d4
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-upload.any.js
@@ -0,0 +1,139 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testUpload(desc, url, method, createBody, expectedBody) {
+ const requestInit = {method};
+ promise_test(function(test){
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ requestInit.duplex = "half";
+ }
+ return fetch(url, requestInit).then(function(resp) {
+ return resp.text().then((text)=> {
+ assert_equals(text, expectedBody);
+ });
+ });
+ }, desc);
+}
+
+function testUploadFailure(desc, url, method, createBody) {
+ const requestInit = {method};
+ promise_test(t => {
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ }
+ return promise_rejects_js(t, TypeError, fetch(url, requestInit));
+ }, desc);
+}
+
+const url = RESOURCES_DIR + "echo-content.py"
+
+testUpload("Fetch with PUT with body", url,
+ "PUT",
+ () => "Request's body",
+ "Request's body");
+testUpload("Fetch with POST with text body", url,
+ "POST",
+ () => "Request's body",
+ "Request's body");
+testUpload("Fetch with POST with URLSearchParams body", url,
+ "POST",
+ () => new URLSearchParams("name=value"),
+ "name=value");
+testUpload("Fetch with POST with Blob body", url,
+ "POST",
+ () => new Blob(["Test"]),
+ "Test");
+testUpload("Fetch with POST with ArrayBuffer body", url,
+ "POST",
+ () => new ArrayBuffer(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Uint8Array body", url,
+ "POST",
+ () => new Uint8Array(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Int8Array body", url,
+ "POST",
+ () => new Int8Array(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float16Array body", url,
+ "POST",
+ () => new Float16Array(2),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float32Array body", url,
+ "POST",
+ () => new Float32Array(1),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float64Array body", url,
+ "POST",
+ () => new Float64Array(1),
+ "\0\0\0\0\0\0\0\0");
+testUpload("Fetch with POST with DataView body", url,
+ "POST",
+ () => new DataView(new ArrayBuffer(8), 0, 4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Blob body with mime type", url,
+ "POST",
+ () => new Blob(["Test"], { type: "text/maybe" }),
+ "Test");
+
+testUploadFailure("Fetch with POST with ReadableStream containing String", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue("Test");
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing null", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(null);
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing number", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(99);
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(new ArrayBuffer());
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing Blob", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(new Blob());
+ controller.close();
+ }})
+ });
+
+promise_test(async (test) => {
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=421&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
+ + `&dispatch=check_partition&addcounter=true`,
+ {method: "POST", body: "foobar"});
+ assert_equals(resp.status, 421);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 2 times. 2 connections were created.");
+}, "Fetch with POST with text body on 421 response should be retried once on new connection.");
+
+promise_test(async (test) => {
+ const body = new ReadableStream({start: c => c.close()});
+ await promise_rejects_js(test, TypeError, fetch('/', {method: 'POST', body}));
+}, "Streaming upload shouldn't work on Http/1.1.");
diff --git a/test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js b/test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js
new file mode 100644
index 00000000000000..68122278ccd2b1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js
@@ -0,0 +1,209 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const duplex = "half";
+
+async function assertUpload(url, method, createBody, expectedBody) {
+ const requestInit = {method};
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ requestInit.duplex = "half";
+ }
+ const resp = await fetch(url, requestInit);
+ const text = await resp.text();
+ assert_equals(text, expectedBody);
+}
+
+function testUpload(desc, url, method, createBody, expectedBody) {
+ promise_test(async () => {
+ await assertUpload(url, method, createBody, expectedBody);
+ }, desc);
+}
+
+function createStream(chunks) {
+ return new ReadableStream({
+ start: (controller) => {
+ for (const chunk of chunks) {
+ controller.enqueue(chunk);
+ }
+ controller.close();
+ }
+ });
+}
+
+const url = RESOURCES_DIR + "echo-content.h2.py"
+
+testUpload("Fetch with POST with empty ReadableStream", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.close();
+ }})
+ },
+ "");
+
+testUpload("Fetch with POST with ReadableStream", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }})
+ },
+ "Test");
+
+promise_test(async (test) => {
+ const body = new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }});
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=421&uuid=${token()}&partition_id=${self.origin}`
+ + `&dispatch=check_partition&addcounter=true`,
+ {method: "POST", body: body, duplex});
+ assert_equals(resp.status, 421);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 1 times. 1 connections were created.");
+}, "Fetch with POST with ReadableStream on 421 response should return the response and not retry.");
+
+promise_test(async (test) => {
+ const request = new Request('', {
+ body: new ReadableStream(),
+ method: 'POST',
+ duplex,
+ });
+
+ assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`);
+
+ const response = await fetch('data:a/a;charset=utf-8,test', {
+ method: 'POST',
+ body: new ReadableStream(),
+ duplex,
+ });
+
+ assert_equals(await response.text(), 'test', `Response has correct body`);
+}, "Feature detect for POST with ReadableStream");
+
+promise_test(async (test) => {
+ const request = new Request('data:a/a;charset=utf-8,test', {
+ body: new ReadableStream(),
+ method: 'POST',
+ duplex,
+ });
+
+ assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`);
+ const response = await fetch(request);
+ assert_equals(await response.text(), 'test', `Response has correct body`);
+}, "Feature detect for POST with ReadableStream, using request object");
+
+test(() => {
+ let duplexAccessed = false;
+
+ const request = new Request("", {
+ body: new ReadableStream(),
+ method: "POST",
+ get duplex() {
+ duplexAccessed = true;
+ return "half";
+ },
+ });
+
+ assert_equals(
+ request.headers.get("Content-Type"),
+ null,
+ `Request should not have a content-type set`
+ );
+ assert_true(duplexAccessed, `duplex dictionary property should be accessed`);
+}, "Synchronous feature detect");
+
+// The asserts the synchronousFeatureDetect isn't broken by a partial implementation.
+// An earlier feature detect was broken by Safari implementing streaming bodies as part of Request,
+// but it failed when passed to fetch().
+// This tests ensures that UAs must not implement RequestInit.duplex and streaming request bodies without also implementing the fetch() parts.
+promise_test(async () => {
+ let duplexAccessed = false;
+
+ const request = new Request("", {
+ body: new ReadableStream(),
+ method: "POST",
+ get duplex() {
+ duplexAccessed = true;
+ return "half";
+ },
+ });
+
+ const supported =
+ request.headers.get("Content-Type") === null && duplexAccessed;
+
+ // If the feature detect fails, assume the browser is being truthful (other tests pick up broken cases here)
+ if (!supported) return false;
+
+ await assertUpload(
+ url,
+ "POST",
+ () =>
+ new ReadableStream({
+ start: (controller) => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ },
+ }),
+ "Test"
+ );
+}, "Synchronous feature detect fails if feature unsupported");
+
+promise_test(async (t) => {
+ const body = createStream(["hello"]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing a String");
+
+promise_test(async (t) => {
+ const body = createStream([null]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing null");
+
+promise_test(async (t) => {
+ const body = createStream([33]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing a number");
+
+promise_test(async (t) => {
+ const url = "/fetch/api/resources/authentication.py?realm=test";
+ const body = createStream([]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload should fail on a 401 response");
+
+promise_test(async (t) => {
+ const abortMessage = 'foo abort';
+ let streamCancelPromise = new Promise(async res => {
+ var stream = new ReadableStream({
+ cancel: function(reason) {
+ res(reason);
+ }
+ });
+ let abortController = new AbortController();
+ let fetchPromise = promise_rejects_exactly(t, abortMessage, fetch('', {
+ method: 'POST',
+ body: stream,
+ duplex: 'half',
+ signal: abortController.signal
+ }));
+ abortController.abort(abortMessage);
+ await fetchPromise;
+ });
+
+ let cancelReason = await streamCancelPromise;
+ assert_equals(
+ cancelReason, abortMessage, 'ReadableStream.cancel should be called.');
+}, 'ReadbleStream should be closed on signal.abort');
diff --git a/test/fixtures/wpt/fetch/api/basic/response-null-body.any.js b/test/fixtures/wpt/fetch/api/basic/response-null-body.any.js
new file mode 100644
index 00000000000000..bb058926572e82
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/response-null-body.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+const nullBodyStatus = [204, 205, 304];
+const methods = ["GET", "POST", "OPTIONS"];
+
+for (const status of nullBodyStatus) {
+ for (const method of methods) {
+ promise_test(
+ async () => {
+ const url =
+ `${RESOURCES_DIR}status.py?code=${status}&content=hello-world`;
+ const resp = await fetch(url, { method });
+ assert_equals(resp.status, status);
+ assert_equals(resp.body, null, "the body should be null");
+ const text = await resp.text();
+ assert_equals(text, "", "null bodies result in empty text");
+ },
+ `Response.body is null for responses with status=${status} (method=${method})`,
+ );
+ }
+}
+
+promise_test(async () => {
+ const url = `${RESOURCES_DIR}status.py?code=200&content=hello-world`;
+ const resp = await fetch(url, { method: "HEAD" });
+ assert_equals(resp.status, 200);
+ assert_equals(resp.body, null, "the body should be null");
+ const text = await resp.text();
+ assert_equals(text, "", "null bodies result in empty text");
+}, `Response.body is null for responses with method=HEAD`);
+
+promise_test(async (t) => {
+ const integrity = "sha384-UT6f7WCFp32YJnp1is4l/ZYnOeQKpE8xjmdkLOwZ3nIP+tmT2aMRFQGJomjVf5cE";
+ const url = `${RESOURCES_DIR}status.py?code=204&content=hello-world`;
+ const promise = fetch(url, { method: "GET", integrity });
+ promise_rejects_js(t, TypeError, promise);
+}, "Null body status with subresource integrity should abort");
diff --git a/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js b/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js
new file mode 100644
index 00000000000000..0d123c429445f1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js
@@ -0,0 +1,16 @@
+function checkResponseURL(fetchedURL, expectedURL)
+{
+ promise_test(function() {
+ return fetch(fetchedURL).then(function(response) {
+ assert_equals(response.url, expectedURL);
+ });
+ }, "Testing response url getter with " +fetchedURL);
+}
+
+var baseURL = "http://{{host}}:{{ports[http][0]}}";
+checkResponseURL(baseURL + "/ada", baseURL + "/ada");
+checkResponseURL(baseURL + "/#", baseURL + "/");
+checkResponseURL(baseURL + "/#ada", baseURL + "/");
+checkResponseURL(baseURL + "#ada", baseURL + "/");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js
new file mode 100644
index 00000000000000..9ef44183c1750a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js
@@ -0,0 +1,26 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkNetworkError(url, method) {
+ method = method || "GET";
+ const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO"
+ promise_test(function(test) {
+ var promise = fetch(url, { method: method });
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+checkNetworkError("about:blank", "GET");
+checkNetworkError("about:blank", "PUT");
+checkNetworkError("about:blank", "POST");
+checkNetworkError("about:invalid.com");
+checkNetworkError("about:config");
+checkNetworkError("about:unicorn");
+
+promise_test(function(test) {
+ var promise = fetch("about:blank", {
+ "method": "GET",
+ "Range": "bytes=1-10"
+ });
+ return promise_rejects_js(test, TypeError, promise);
+}, "Fetching about:blank with range header does not affect behavior");
diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js
new file mode 100644
index 00000000000000..8afdc033c9d7dd
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js
@@ -0,0 +1,125 @@
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, size, desc) {
+ promise_test(function(test) {
+ size = size.toString();
+ return fetch(url).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+ assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length"));
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, data, "Response's body is " + data);
+ });
+ }, desc);
+}
+
+var blob = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain", blob.size,
+ "Fetching [GET] URL.createObjectURL(blob) is OK");
+
+function checkKoUrl(url, method, desc) {
+ promise_test(function(test) {
+ var promise = fetch(url, {"method": method});
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET",
+ "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO");
+
+var invalidRequestMethods = [
+ "POST",
+ "OPTIONS",
+ "HEAD",
+ "PUT",
+ "DELETE",
+ "INVALID",
+];
+invalidRequestMethods.forEach(function(method) {
+ checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO");
+});
+
+checkKoUrl("blob:not-backed-by-a-blob/", "GET",
+ "Fetching [GET] blob:not-backed-by-a-blob/ is KO");
+
+let empty_blob = new Blob([]);
+checkFetchResponse(URL.createObjectURL(empty_blob), "", "", 0,
+ "Fetching URL.createObjectURL(empty_blob) is OK");
+
+let empty_type_blob = new Blob([], {type: ""});
+checkFetchResponse(URL.createObjectURL(empty_type_blob), "", "", 0,
+ "Fetching URL.createObjectURL(empty_type_blob) is OK");
+
+let empty_data_blob = new Blob([], {type: "text/plain"});
+checkFetchResponse(URL.createObjectURL(empty_data_blob), "", "text/plain", 0,
+ "Fetching URL.createObjectURL(empty_data_blob) is OK");
+
+let invalid_type_blob = new Blob([], {type: "invalid"});
+checkFetchResponse(URL.createObjectURL(invalid_type_blob), "", "", 0,
+ "Fetching URL.createObjectURL(invalid_type_blob) is OK");
+
+promise_test(function(test) {
+ return fetch("/images/blue.png").then(function(resp) {
+ return resp.arrayBuffer();
+ }).then(function(image_buffer) {
+ let blob = new Blob([image_buffer]);
+ return fetch(URL.createObjectURL(blob)).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "", "Content-Type is " + resp.headers.get("Content-Type"));
+ })
+ });
+}, "Blob content is not sniffed for a content type [image/png]");
+
+let simple_xml_string = '';
+let xml_blob_no_type = new Blob([simple_xml_string]);
+checkFetchResponse(URL.createObjectURL(xml_blob_no_type), simple_xml_string, "", 45,
+ "Blob content is not sniffed for a content type [text/xml]");
+
+let simple_text_string = 'Hello, World!';
+promise_test(function(test) {
+ let blob = new Blob([simple_text_string], {"type": "text/plain"});
+ let slice = blob.slice(7, simple_text_string.length, "\0");
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "6");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, "World!");
+ });
+}, "Set content type to the empty string for slice with invalid content type");
+
+promise_test(function(test) {
+ let blob = new Blob([simple_text_string], {"type": "text/plain"});
+ let slice = blob.slice(7, simple_text_string.length, "\0");
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "6");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, "World!");
+ });
+}, "Set content type to the empty string for slice with no content type ");
+
+promise_test(function(test) {
+ let blob = new Blob([simple_xml_string]);
+ let slice = blob.slice(0, 38);
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "38");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, '');
+ });
+}, "Blob.slice should not sniff the content for a content type");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js
new file mode 100644
index 00000000000000..55df43bd503ce4
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js
@@ -0,0 +1,43 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, fetchMode, method) {
+ var cut = (url.length >= 40) ? "[...]" : "";
+ var desc = "Fetching " + (method ? "[" + method + "] " : "") + url.substring(0, 40) + cut + " is OK";
+ var init = {"method": method || "GET"};
+ if (fetchMode) {
+ init.mode = fetchMode;
+ desc += " (" + fetchMode + ")";
+ }
+ promise_test(function(test) {
+ return fetch(url, init).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.statusText, "OK", "HTTP statusText is OK");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+ return resp.text();
+ }).then(function(body) {
+ assert_equals(body, data, "Response's body is correct");
+ });
+ }, desc);
+}
+
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "same-origin");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "cors");
+checkFetchResponse("data:text/plain;base64,cmVzcG9uc2UncyBib2R5", "response's body", "text/plain");
+checkFetchResponse("data:image/png;base64,cmVzcG9uc2UncyBib2R5",
+ "response's body",
+ "image/png");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", null, "POST");
+checkFetchResponse("data:,response%27s%20body", "", "text/plain;charset=US-ASCII", null, "HEAD");
+
+function checkKoUrl(url, method, desc) {
+ var cut = (url.length >= 40) ? "[...]" : "";
+ desc = "Fetching [" + method + "] " + url.substring(0, 45) + cut + " is KO"
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, {"method": method}));
+ }, desc);
+}
+
+checkKoUrl("data:notAdataUrl.com", "GET");
diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js
new file mode 100644
index 00000000000000..550f69c41b5a43
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js
@@ -0,0 +1,31 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkKoUrl(url, desc) {
+ if (!desc)
+ desc = "Fetching " + url.substring(0, 45) + " is KO"
+ promise_test(function(test) {
+ var promise = fetch(url);
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/";
+checkKoUrl("aaa" + urlWithoutScheme);
+checkKoUrl("cap" + urlWithoutScheme);
+checkKoUrl("cid" + urlWithoutScheme);
+checkKoUrl("dav" + urlWithoutScheme);
+checkKoUrl("dict" + urlWithoutScheme);
+checkKoUrl("dns" + urlWithoutScheme);
+checkKoUrl("geo" + urlWithoutScheme);
+checkKoUrl("im" + urlWithoutScheme);
+checkKoUrl("imap" + urlWithoutScheme);
+checkKoUrl("ipp" + urlWithoutScheme);
+checkKoUrl("ldap" + urlWithoutScheme);
+checkKoUrl("mailto" + urlWithoutScheme);
+checkKoUrl("nfs" + urlWithoutScheme);
+checkKoUrl("pop" + urlWithoutScheme);
+checkKoUrl("rtsp" + urlWithoutScheme);
+checkKoUrl("snmp" + urlWithoutScheme);
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/basic/status.h2.any.js b/test/fixtures/wpt/fetch/api/basic/status.h2.any.js
new file mode 100644
index 00000000000000..99fec88f505db8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/status.h2.any.js
@@ -0,0 +1,17 @@
+// See also /xhr/status.h2.window.js
+
+[
+ 200,
+ 210,
+ 400,
+ 404,
+ 410,
+ 500,
+ 502
+].forEach(status => {
+ promise_test(async t => {
+ const response = await fetch("/xhr/resources/status.py?code=" + status);
+ assert_equals(response.status, status, "status should be " + status);
+ assert_equals(response.statusText, "", "statusText should be the empty string");
+ }, "statusText over H2 for status " + status + " should be the empty string");
+});
diff --git a/test/fixtures/wpt/fetch/api/basic/stream-response.any.js b/test/fixtures/wpt/fetch/api/basic/stream-response.any.js
new file mode 100644
index 00000000000000..d964dda717cfb6
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/stream-response.any.js
@@ -0,0 +1,40 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function streamBody(reader, test, count = 0) {
+ return reader.read().then(function(data) {
+ if (!data.done && count < 2) {
+ count += 1;
+ return streamBody(reader, test, count);
+ } else {
+ test.step(function() {
+ assert_true(count >= 2, "Retrieve body progressively");
+ });
+ }
+ });
+}
+
+//simulate streaming:
+//count is large enough to let the UA deliver the body before it is completely retrieved
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(resp) {
+ if (resp.body)
+ return streamBody(resp.body.getReader(), test);
+ else
+ test.step(function() {
+ assert_unreached( "Body does not exist in response");
+ });
+ });
+}, "Stream response's body when content-type is present");
+
+// This test makes sure that the response body is not buffered if no content type is provided.
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=300&count=10¬ype=true").then(function(resp) {
+ if (resp.body)
+ return streamBody(resp.body.getReader(), test);
+ else
+ test.step(function() {
+ assert_unreached( "Body does not exist in response");
+ });
+ });
+}, "Stream response's body when content-type is not present");
diff --git a/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js b/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js
new file mode 100644
index 00000000000000..382efc1a8b4206
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js
@@ -0,0 +1,54 @@
+// META: global=window,worker
+
+// These tests verify that stream creation is not affected by changes to
+// Object.prototype.
+
+const creationCases = {
+ fetch: async () => fetch(location.href),
+ request: () => new Request(location.href, {method: 'POST', body: 'hi'}),
+ response: () => new Response('bye'),
+ consumeEmptyResponse: () => new Response().text(),
+ consumeNonEmptyResponse: () => new Response(new Uint8Array([64])).text(),
+ consumeEmptyRequest: () => new Request(location.href).text(),
+ consumeNonEmptyRequest: () => new Request(location.href,
+ {method: 'POST', body: 'yes'}).arrayBuffer(),
+};
+
+for (const creationCase of Object.keys(creationCases)) {
+ for (const accessorName of ['start', 'type', 'size', 'highWaterMark']) {
+ promise_test(async t => {
+ Object.defineProperty(Object.prototype, accessorName, {
+ get() { throw Error(`Object.prototype.${accessorName} was accessed`); },
+ configurable: true
+ });
+ t.add_cleanup(() => {
+ delete Object.prototype[accessorName];
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `throwing Object.prototype.${accessorName} accessor should not affect ` +
+ `stream creation by '${creationCase}'`);
+
+ promise_test(async t => {
+ // -1 is a convenient value which is invalid, and should cause the
+ // constructor to throw, for all four fields.
+ Object.prototype[accessorName] = -1;
+ t.add_cleanup(() => {
+ delete Object.prototype[accessorName];
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `Object.prototype.${accessorName} accessor returning invalid value ` +
+ `should not affect stream creation by '${creationCase}'`);
+ }
+
+ promise_test(async t => {
+ Object.prototype.start = controller => controller.error(new Error('start'));
+ t.add_cleanup(() => {
+ delete Object.prototype.start;
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `Object.prototype.start function which errors the stream should not ` +
+ `affect stream creation by '${creationCase}'`);
+}
diff --git a/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js b/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js
new file mode 100644
index 00000000000000..05c8c88825d37b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js
@@ -0,0 +1,74 @@
+// META: title=Fetch: Request and Response text() should decode as UTF-8
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function testTextDecoding(body, expectedText, urlParameter, title)
+{
+ var arrayBuffer = stringToArray(body);
+
+ promise_test(function(test) {
+ var request = new Request("", {method: "POST", body: arrayBuffer});
+ return request.text().then(function(value) {
+ assert_equals(value, expectedText, "Request.text() should decode data as UTF-8");
+ });
+ }, title + " with Request.text()");
+
+ promise_test(function(test) {
+ var response = new Response(arrayBuffer);
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Response.text() should decode data as UTF-8");
+ });
+ }, title + " with Response.text()");
+
+ promise_test(function(test) {
+ return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=" + urlParameter).then(function(response) {
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8");
+ });
+ });
+ }, title + " with fetched data (UTF-8 charset)");
+
+ promise_test(function(test) {
+ return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=" + urlParameter).then(function(response) {
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8");
+ });
+ });
+ }, title + " with fetched data (UTF-16 charset)");
+
+ promise_test(function(test) {
+ return new Response(body).arrayBuffer().then(function(buffer) {
+ assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Response.arrayBuffer() should contain data encoded as UTF-8");
+ });
+ }, title + " (Response object)");
+
+ promise_test(function(test) {
+ return new Request("", {method: "POST", body: body}).arrayBuffer().then(function(buffer) {
+ assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Request.arrayBuffer() should contain data encoded as UTF-8");
+ });
+ }, title + " (Request object)");
+
+}
+
+var utf8WithBOM = "\xef\xbb\xbf\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90";
+var utf8WithBOMAsURLParameter = "%EF%BB%BF%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90";
+var utf8WithoutBOM = "\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90";
+var utf8WithoutBOMAsURLParameter = "%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90";
+var utf8Decoded = "三村かな子";
+testTextDecoding(utf8WithBOM, utf8Decoded, utf8WithBOMAsURLParameter, "UTF-8 with BOM");
+testTextDecoding(utf8WithoutBOM, utf8Decoded, utf8WithoutBOMAsURLParameter, "UTF-8 without BOM");
+
+var utf16BEWithBOM = "\xfe\xff\x4e\x09\x67\x51\x30\x4b\x30\x6a\x5b\x50";
+var utf16BEWithBOMAsURLParameter = "%fe%ff%4e%09%67%51%30%4b%30%6a%5b%50";
+var utf16BEWithBOMDecodedAsUTF8 = "��N\tgQ0K0j[P";
+testTextDecoding(utf16BEWithBOM, utf16BEWithBOMDecodedAsUTF8, utf16BEWithBOMAsURLParameter, "UTF-16BE with BOM decoded as UTF-8");
+
+var utf16LEWithBOM = "\xff\xfe\x09\x4e\x51\x67\x4b\x30\x6a\x30\x50\x5b";
+var utf16LEWithBOMAsURLParameter = "%ff%fe%09%4e%51%67%4b%30%6a%30%50%5b";
+var utf16LEWithBOMDecodedAsUTF8 = "��\tNQgK0j0P[";
+testTextDecoding(utf16LEWithBOM, utf16LEWithBOMDecodedAsUTF8, utf16LEWithBOMAsURLParameter, "UTF-16LE with BOM decoded as UTF-8");
+
+var utf16WithoutBOM = "\xe6\x00\xf8\x00\xe5\x00\x0a\x00\xc6\x30\xb9\x30\xc8\x30\x0a\x00";
+var utf16WithoutBOMAsURLParameter = "%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00";
+var utf16WithoutBOMDecoded = "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000";
+testTextDecoding(utf16WithoutBOM, utf16WithoutBOMDecoded, utf16WithoutBOMAsURLParameter, "UTF-16 without BOM decoded as UTF-8");
diff --git a/test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html b/test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html
new file mode 100644
index 00000000000000..fa47b29473af21
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/body/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/body/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/body/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/body/cloned-any.js b/test/fixtures/wpt/fetch/api/body/cloned-any.js
new file mode 100644
index 00000000000000..2bca96c7043db3
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/body/cloned-any.js
@@ -0,0 +1,50 @@
+// Changing the body after it have been passed to Response/Request
+// should not change the outcome of the consumed body
+
+const url = 'http://a';
+const method = 'post';
+
+promise_test(async t => {
+ const body = new FormData();
+ body.set('a', '1');
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body.set('a', '2');
+ assert_true((await res.formData()).get('a') === '1');
+ assert_true((await req.formData()).get('a') === '1');
+}, 'FormData is cloned');
+
+promise_test(async t => {
+ const body = new URLSearchParams({a: '1'});
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body.set('a', '2');
+ assert_true((await res.formData()).get('a') === '1');
+ assert_true((await req.formData()).get('a') === '1');
+}, 'URLSearchParams is cloned');
+
+promise_test(async t => {
+ const body = new Uint8Array([97]); // a
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body[0] = 98; // b
+ assert_true(await res.text() === 'a');
+ assert_true(await req.text() === 'a');
+}, 'TypedArray is cloned');
+
+promise_test(async t => {
+ const body = new Uint8Array([97]); // a
+ const res = new Response(body.buffer);
+ const req = new Request(url, { method, body: body.buffer });
+ body[0] = 98; // b
+ assert_true(await res.text() === 'a');
+ assert_true(await req.text() === 'a');
+}, 'ArrayBuffer is cloned');
+
+promise_test(async t => {
+ const body = new Blob(['a']);
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ assert_true(await res.blob() !== body);
+ assert_true(await req.blob() !== body);
+}, 'Blob is cloned');
diff --git a/test/fixtures/wpt/fetch/api/body/formdata.any.js b/test/fixtures/wpt/fetch/api/body/formdata.any.js
new file mode 100644
index 00000000000000..6733fa0ed70afe
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/body/formdata.any.js
@@ -0,0 +1,25 @@
+promise_test(async t => {
+ const res = new Response(new FormData());
+ const fd = await res.formData();
+ assert_true(fd instanceof FormData);
+}, 'Consume empty response.formData() as FormData');
+
+promise_test(async t => {
+ const req = new Request('about:blank', {
+ method: 'POST',
+ body: new FormData()
+ });
+ const fd = await req.formData();
+ assert_true(fd instanceof FormData);
+}, 'Consume empty request.formData() as FormData');
+
+promise_test(async t => {
+ let formdata = new FormData();
+ formdata.append('foo', new Blob([JSON.stringify({ bar: "baz", })], { type: "application/json" }));
+ let blob = await new Response(formdata).blob();
+ let body = await blob.text();
+ blob = new Blob([body.toLowerCase()], { type: blob.type.toLowerCase() });
+ let formdataWithLowercaseBody = await new Response(blob).formData();
+ assert_true(formdataWithLowercaseBody.has("foo"));
+ assert_equals(formdataWithLowercaseBody.get("foo").type, "application/json");
+}, 'Consume multipart/form-data headers case-insensitively');
diff --git a/test/fixtures/wpt/fetch/api/body/mime-type.any.js b/test/fixtures/wpt/fetch/api/body/mime-type.any.js
new file mode 100644
index 00000000000000..ed19309bdb24f1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/body/mime-type.any.js
@@ -0,0 +1,127 @@
+[
+ () => new Request("about:blank", { headers: { "Content-Type": "text/plain" } }),
+ () => new Response("", { headers: { "Content-Type": "text/plain" } })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ const newMIMEType = "test/test";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: overriding explicit Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new URLSearchParams(), method: "POST" }),
+ () => new Response(new URLSearchParams()),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "application/x-www-form-urlencoded;charset=UTF-8");
+ bodyContainer.headers.delete("Content-Type");
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ }, `${bodyContainer.constructor.name}: removing implicit Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new ArrayBuffer(), method: "POST" }),
+ () => new Response(new ArrayBuffer()),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), null);
+ const newMIMEType = "test/test";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: setting missing Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST" }),
+ () => new Response(),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST", headers: [["Content-Type", "Mytext/Plain"]] }),
+ () => new Response("", { headers: [["Content-Type", "Mytext/Plain"]] })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, 'mytext/plain');
+ }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body with Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([""]), method: "POST" }),
+ () => new Response(new Blob([""]))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ assert_equals(bodyContainer.headers.get("Content-Type"), null);
+ }, `${bodyContainer.constructor.name}: MIME type for Blob`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([""], { type: "Text/Plain" }), method: "POST" }),
+ () => new Response(new Blob([""], { type: "Text/Plain" }))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "text/plain");
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ }, `${bodyContainer.constructor.name}: MIME type for Blob with non-empty type`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST", body: new Blob([""], { type: "Text/Plain" }), headers: [["Content-Type", "Text/Html"]] }),
+ () => new Response(new Blob([""], { type: "Text/Plain" }), { headers: [["Content-Type", "Text/Html"]] })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ const cloned = bodyContainer.clone();
+ promise_test(async t => {
+ const blobs = [await bodyContainer.blob(), await cloned.blob()];
+ assert_equals(blobs[0].type, "text/html");
+ assert_equals(blobs[1].type, "text/html");
+ assert_equals(bodyContainer.headers.get("Content-Type"), "Text/Html");
+ assert_equals(cloned.headers.get("Content-Type"), "Text/Html");
+ }, `${bodyContainer.constructor.name}: Extract a MIME type with clone`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST", headers: [["Content-Type", "text/html"]] }),
+ () => new Response(new Blob([], { type: "text/plain" }), { headers: [["Content-Type", "text/html"]] }),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/html");
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "text/html");
+ }, `${bodyContainer.constructor.name}: Content-Type in headers wins Blob"s type`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST" }),
+ () => new Response(new Blob([], { type: "text/plain" })),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ const newMIMEType = "text/html";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: setting missing Content-Type in headers and it wins Blob"s type`);
+});
diff --git a/test/fixtures/wpt/fetch/api/cors/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/cors/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js b/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js
new file mode 100644
index 00000000000000..95de0af2d8f3b0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js
@@ -0,0 +1,43 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const {
+ HTTPS_ORIGIN,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTPS_REMOTE_ORIGIN,
+} = get_host_info();
+
+function cors(desc, origin) {
+ const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`;
+ const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`;
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {'mode': 'no-cors'}).then((resp) => {
+ assert_equals(resp.status, 0, "Opaque filter: status is 0");
+ assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+ assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+ return resp.text().then((value) => {
+ assert_equals(value, "", "Opaque response should have an empty body");
+ });
+ });
+ }, `${desc} [no-cors mode]`);
+
+ promise_test((test) => {
+ return promise_rejects_js(test, TypeError, fetch(url, {'mode': 'cors'}));
+ }, `${desc} [server forbid CORS]`);
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {'mode': 'cors'}).then((resp) => {
+ assert_equals(resp.status, 200, "Fetch's response's status is 200");
+ assert_equals(resp.type , "cors", "CORS response's type is cors");
+ });
+ }, `${desc} [cors mode]`);
+}
+
+cors('Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT);
+cors('Same domain different protocol different port', HTTPS_ORIGIN);
+cors('Cross domain basic usage', HTTP_REMOTE_ORIGIN);
+cors('Cross domain different port', HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT);
+cors('Cross domain different protocol', HTTPS_REMOTE_ORIGIN);
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js
new file mode 100644
index 00000000000000..f5217b42460a57
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js
@@ -0,0 +1,49 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie";
+
+var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)";
+
+urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)";
+urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)";
+
+urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)";
+urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)";
+
+promise_test(async (test) => {
+ await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"});
+ await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"});
+}, "Set cookies");
+
+function doTest(usePreflight) {
+ promise_test(async (test) => {
+ var url = redirectUrl;
+ var uuid_token = token();
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=301";
+ urlParameters += "&location=" + encodeURIComponent(urlCheckCookies);
+ urlParameters += "&allow_headers=a&headers=Cookie";
+ headers = [];
+ if (usePreflight)
+ headers.push(["a", "b"]);
+
+ var requestInit = {"credentials": "include", "mode": "cors", "headers": headers};
+ var response = await fetch(url + urlParameters, requestInit);
+
+ assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)");
+ }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight");
+}
+
+doTest(false);
+doTest(true);
+
+promise_test(async (test) => {
+ await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"});
+ await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"});
+}, "Clean cookies");
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js b/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js
new file mode 100644
index 00000000000000..8c666e4782f4c8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js
@@ -0,0 +1,56 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) {
+ var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+ var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie";
+ //enable cors with credentials
+ var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+ urlParameters += "|header(Access-Control-Allow-Credentials,true)";
+
+ var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+ urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)";
+ if (cookies) {
+ urlParameters += "|header(Set-Cookie,";
+ urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)";
+ urlCleanParameters += "|header(Set-Cookie,";
+ urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)";
+ }
+
+ var requestInit = {"credentials": credentialsMode, "mode": "cors"};
+
+ promise_test(function(test){
+ return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ //check cookies sent
+ return fetch(urlCheckCookies, requestInit);
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response");
+ if (credentialsMode === "include" && baseURL1 === baseURL2) {
+ assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)");
+ }
+ else {
+ assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie");
+ }
+ //clean cookies
+ return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"});
+ }).catch(function(e) {
+ return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) {
+ throw e;
+ })
+ });
+ }, desc);
+}
+
+var local = get_host_info().HTTP_ORIGIN;
+var remote = get_host_info().HTTP_REMOTE_ORIGIN;
+// FIXME: otherRemote might not be accessible on some test environments.
+var otherRemote = local.replace("http://", "http://www.");
+
+corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]);
+corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]);
+corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]);
+corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]);
+corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]);
+corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]);
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js
new file mode 100644
index 00000000000000..340e99ab5f99d7
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js
@@ -0,0 +1,41 @@
+// META: script=../resources/utils.js
+
+const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt",
+ sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|"
+
+promise_test(() => {
+ const headers = "header(Access-Control-Allow-Origin,*)"
+ return fetch(url + sharedHeaders + headers).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("test"), "X")
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "Basic Access-Control-Expose-Headers: * support")
+
+promise_test(() => {
+ const origin = location.origin, // assuming an ASCII origin
+ headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)"
+ return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted
+ assert_equals(resp.headers.get("test"), null)
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "* for credentialed fetches only matches literally")
+
+promise_test(() => {
+ const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)"
+ return fetch(url + sharedHeaders + headers).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("test"), "X")
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "* can be one of several values")
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js
new file mode 100644
index 00000000000000..5f9492487f136f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js
@@ -0,0 +1,65 @@
+// META: script=../resources/utils.js
+
+function corsFilter(corsUrl, headerName, headerValue, isFiltered) {
+ var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)";
+ promise_test(function(test) {
+ return fetch(url).then(function(resp) {
+ assert_equals(resp.status, 200, "Fetch success with code 200");
+ assert_equals(resp.type , "cors", "CORS fetch's response has cors type");
+ if (!isFiltered) {
+ assert_equals(resp.headers.get(headerName), headerValue,
+ headerName + " header should be included in response with value: " + headerValue);
+ } else {
+ assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response");
+ }
+ });
+ }, "CORS filter on " + headerName + " header");
+}
+
+function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) {
+ var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" +
+ "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" +
+ "header(Access-Control-Allow-Credentials, true)" +
+ "header(Access-Control-Expose-Headers," + headerName + ")";
+
+ var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed");
+ if (withCredentials)
+ title+= "(credentials = include)";
+ promise_test(function(test) {
+ return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) {
+ assert_equals(resp.status, 200, "Fetch success with code 200");
+ assert_equals(resp.type , "cors", "CORS fetch's response has cors type");
+ if (!isForbidden) {
+ assert_equals(resp.headers.get(headerName), headerValue,
+ headerName + " header should be included in response with value: " + headerValue);
+ } else {
+ assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response");
+ }
+ });
+ }, title);
+}
+
+var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+
+corsFilter(url, "Cache-Control", "no-cache", false);
+corsFilter(url, "Content-Language", "fr", false);
+corsFilter(url, "Content-Type", "text/html", false);
+corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false);
+corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false);
+corsFilter(url, "Pragma", "no-cache", false);
+corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top"
+
+corsFilter(url, "Age", "27", true);
+corsFilter(url, "Server", "wptServe" , true);
+corsFilter(url, "Warning", "Mind the gap" , true);
+corsFilter(url, "Set-Cookie", "name=value; max-age=0", true);
+corsFilter(url, "Set-Cookie2", "name=value; max-age=0", true);
+
+corsExposeFilter(url, "Age", "27", false);
+corsExposeFilter(url, "Server", "wptServe" , false);
+corsExposeFilter(url, "Warning", "Mind the gap" , false);
+
+corsExposeFilter(url, "Set-Cookie", "name=value; max-age=0" , true);
+corsExposeFilter(url, "Set-Cookie2", "name=value; max-age=0" , true);
+corsExposeFilter(url, "Set-Cookie", "name=value; max-age=0" , true, true);
+corsExposeFilter(url, "Set-Cookie2", "name=value; max-age=0" , true, true);
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js b/test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js
new file mode 100644
index 00000000000000..f54bf4f9b602f6
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js
@@ -0,0 +1,116 @@
+// META: global=window
+// META: timeout=long
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+// META: script=../resources/utils.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTPS_ORIGIN,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTPS_REMOTE_ORIGIN,
+} = get_host_info();
+
+/**
+ * Tests to cover the basic behaviors of keepalive + cors/no-cors mode requests
+ * to different `origin` when the initiator document is still alive. They should
+ * behave the same as without setting keepalive.
+ */
+function keepaliveCorsBasicTest(desc, origin) {
+ const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`;
+ const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`;
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {keepalive: true, 'mode': 'no-cors'})
+ .then((resp) => {
+ assert_equals(resp.status, 0, 'Opaque filter: status is 0');
+ assert_equals(resp.statusText, '', 'Opaque filter: statusText is ""');
+ assert_equals(
+ resp.type, 'opaque', 'Opaque filter: response\'s type is opaque');
+ return resp.text().then((value) => {
+ assert_equals(
+ value, '', 'Opaque response should have an empty body');
+ });
+ });
+ }, `${desc} [no-cors mode]`);
+
+ promise_test((test) => {
+ return promise_rejects_js(
+ test, TypeError, fetch(url, {keepalive: true, 'mode': 'cors'}));
+ }, `${desc} [cors mode, server forbid CORS]`);
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {keepalive: true, 'mode': 'cors'})
+ .then((resp) => {
+ assert_equals(resp.status, 200, 'Fetch\'s response\'s status is 200');
+ assert_equals(resp.type, 'cors', 'CORS response\'s type is cors');
+ });
+ }, `${desc} [cors mode]`);
+}
+
+keepaliveCorsBasicTest(
+ `[keepalive] Same domain different port`, HTTP_ORIGIN_WITH_DIFFERENT_PORT);
+keepaliveCorsBasicTest(
+ `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain different port`,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN);
+
+/**
+ * In a same-site iframe, and in `unload` event handler, test to fetch
+ * a keepalive URL that involves in different cors modes.
+ */
+function keepaliveCorsInUnloadTest(description, origin, method) {
+ const evt = 'unload';
+ for (const mode of ['no-cors', 'cors']) {
+ for (const disallowCrossOrigin of [false, true]) {
+ const desc = `${description} ${method} request in ${evt} [${mode} mode` +
+ (disallowCrossOrigin ? ']' : ', server forbid CORS]');
+ const expectTokenExist = !disallowCrossOrigin || mode === 'no-cors';
+ promise_test(async (test) => {
+ const token1 = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveIframeUrl(token1, method, {
+ frameOrigin: '',
+ requestOrigin: origin,
+ sendOn: evt,
+ mode: mode,
+ disallowCrossOrigin
+ });
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ iframe.remove();
+ assert_equals(await getTokenFromMessage(), token1);
+
+ assertStashedTokenAsync(desc, token1, {expectTokenExist});
+ }, `${desc}; setting up`);
+ }
+ }
+}
+
+for (const method of ['GET', 'POST']) {
+ keepaliveCorsInUnloadTest(
+ '[keepalive] Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN,
+ method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN, method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain different port`,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN,
+ method);
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js
new file mode 100644
index 00000000000000..b3abb922841c63
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function corsMultipleOrigins(originList) {
+ var urlParameters = "?origin=" + encodeURIComponent(originList.join(", "));
+ var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters));
+ }, "Listing multiple origins is illegal: " + originList);
+}
+/* Actual origin */
+var origin = "http://{{host}}:{{ports[http][0]}}";
+
+corsMultipleOrigins(["\"\"", "http://example.com", origin]);
+corsMultipleOrigins(["\"\"", "http://example.com", "*"]);
+corsMultipleOrigins(["\"\"", origin, origin]);
+corsMultipleOrigins(["*", "http://example.com", "*"]);
+corsMultipleOrigins(["*", "http://example.com", origin]);
+corsMultipleOrigins(["", "http://example.com", "https://example2.com"]);
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js
new file mode 100644
index 00000000000000..7a0269aae4ec3d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js
@@ -0,0 +1,41 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsNoPreflight(desc, baseURL, method, headerName, headerValue) {
+
+ var uuid_token = token();
+ var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "method": method, "headers":{}};
+ if (headerName)
+ requestInit["headers"][headerName] = headerValue;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made");
+ });
+ });
+ }, desc);
+}
+
+var host_info = get_host_info();
+
+corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET");
+corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET");
+corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET");
+corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET");
+corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET");
+corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST");
+corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD");
+corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*");
+corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr");
+corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr");
+corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded");
+corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data");
+corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain");
+corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8");
+corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8");
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js b/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js
new file mode 100644
index 00000000000000..30a02d910fdad5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js
@@ -0,0 +1,51 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+/* If origin is undefined, it is set to fetched url's origin*/
+function corsOrigin(desc, baseURL, method, origin, shouldPass) {
+ if (!origin)
+ origin = baseURL;
+
+ var uuid_token = token();
+ var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method;
+ var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+ var requestInit = {"mode": "cors", "method": method};
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ if (shouldPass) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ }
+ });
+ }, desc);
+
+}
+
+var host_info = get_host_info();
+
+/* Actual origin */
+var origin = host_info.HTTP_ORIGIN;
+
+corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true);
+corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false);
+corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true);
+corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false);
+corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true);
+corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false);
+corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true);
+corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false);
+corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true);
+corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false);
+corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true);
+corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false);
+corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true);
+corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false);
+corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true);
+corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false);
+corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false);
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js
new file mode 100644
index 00000000000000..ce6a169d814675
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js
@@ -0,0 +1,46 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+var cors_url = get_host_info().HTTP_REMOTE_ORIGIN +
+ dirname(location.pathname) +
+ RESOURCES_DIR +
+ "preflight.py";
+
+promise_test((test) => {
+ var uuid_token = token();
+ var request_url =
+ cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" +
+ "&allow_headers=x-test-header";
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash")
+ .then(() => {
+ return fetch(
+ new Request(request_url,
+ {
+ mode: "cors",
+ method: "POST",
+ headers: [["x-test-header", "test1"]]
+ }));
+ })
+ .then((resp) => {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash");
+ })
+ .then((res) => res.text())
+ .then((txt) => {
+ assert_equals(txt, "1", "Server stash must be cleared.");
+ return fetch(
+ new Request(request_url,
+ {
+ mode: "cors",
+ method: "POST",
+ headers: [["x-test-header", "test2"]]
+ }));
+ })
+ .then((resp) => {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made");
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash");
+ });
+});
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js
new file mode 100644
index 00000000000000..b2747ccd5bc09e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js
@@ -0,0 +1,19 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/corspreflight.js
+
+const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…");
+
+function runTests(testArray) {
+ testArray.forEach(testItem => {
+ const [headerName, headerValue] = testItem;
+ corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header",
+ corsURL,
+ "GET",
+ true,
+ [[headerName, headerValue]]);
+ });
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js
new file mode 100644
index 00000000000000..15f7659abd2156
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js
@@ -0,0 +1,37 @@
+// META: global=window,worker
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) {
+ var uuid_token = token();
+ var url = redirectUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ if (redirectPreflight)
+ urlParameters += "&redirect_preflight";
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+ urlParameters += "&allow_headers=x-force-preflight";
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ });
+ }, desc);
+}
+
+var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+for (var code of [301, 302, 303, 307, 308]) {
+ /* preflight should not follow the redirection */
+ corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true);
+ /* preflight is done before redirection: preflight force redirect to error */
+ corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false);
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js
new file mode 100644
index 00000000000000..5df9fcf1429a7a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js
@@ -0,0 +1,51 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy};
+
+ if (referrer)
+ requestInit.referrer = referrer;
+
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+ urlParameters += "&allow_headers=x-force-preflight";
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct");
+ assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct");
+ assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value");
+ });
+ });
+ }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default"));
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+var origin = get_host_info().HTTP_ORIGIN + "/";
+
+corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, "");
+corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", "");
+
+corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin);
+corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString())
+corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString());
+
+corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin);
+corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin);
+corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString());
+corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString());
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js
new file mode 100644
index 00000000000000..718e351c1d3f09
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js
@@ -0,0 +1,33 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var requestInit = {"mode": "cors"};
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&allow_headers=x-force-preflight";
+ if (allowHeaders)
+ urlParameters += "," + allowHeaders;
+ if (allowMethods)
+ urlParameters += "&allow_methods="+ allowMethods;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+
+ return fetch(url + urlParameters).then(function(resp) {
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ });
+ }, desc);
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null);
+corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value");
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js
new file mode 100644
index 00000000000000..f9fb20469cffa3
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js
@@ -0,0 +1,86 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py",
+ origin = location.origin // assuming an ASCII origin
+
+function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) {
+ return promise_test(t => {
+ let testURL = url + "?",
+ requestInit = {}
+ if (withCredentials) {
+ testURL += "origin=" + origin + "&"
+ testURL += "credentials&"
+ requestInit.credentials = "include"
+ }
+ if (useMethod) {
+ requestInit.method = useMethod
+ }
+ if (useHeader.length > 0) {
+ requestInit.headers = [useHeader]
+ }
+ testURL += "allow_methods=" + allowMethod + "&"
+ testURL += "allow_headers=" + allowHeader + "&"
+
+ if (succeeds) {
+ return fetch(testURL, requestInit).then(resp => {
+ assert_equals(resp.headers.get("x-origin"), origin)
+ })
+ } else {
+ return promise_rejects_js(t, TypeError, fetch(testURL, requestInit))
+ }
+ }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")")
+}
+
+// "GET" does not pass the case-sensitive method check, but in the safe list.
+preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"])
+// Headers check is case-insensitive, and "*" works as any for method.
+preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"])
+// "*" works as any only without credentials.
+preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"])
+preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"])
+preflightTest(false, true, "*", "", "PUT", [])
+preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"])
+preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"])
+// Exact character match works even for "*" with credentials.
+preflightTest(true, true, "*", "*", "*", ["*", "1"])
+
+// The following methods are upper-cased for init["method"] by
+// https://fetch.spec.whatwg.org/#concept-method-normalize
+// but not in Access-Control-Allow-Methods response.
+// But they are https://fetch.spec.whatwg.org/#cors-safelisted-method,
+// CORS anyway passes regardless of the cases.
+for (const METHOD of ['GET', 'HEAD', 'POST']) {
+ const method = METHOD.toLowerCase();
+ preflightTest(true, true, METHOD, "*", METHOD, [])
+ preflightTest(true, true, METHOD, "*", method, [])
+ preflightTest(true, true, method, "*", METHOD, [])
+ preflightTest(true, true, method, "*", method, [])
+}
+
+// The following methods are upper-cased for init["method"] by
+// https://fetch.spec.whatwg.org/#concept-method-normalize
+// but not in Access-Control-Allow-Methods response.
+// As they are not https://fetch.spec.whatwg.org/#cors-safelisted-method,
+// Access-Control-Allow-Methods should contain upper-cased methods,
+// while init["method"] can be either in upper or lower case.
+for (const METHOD of ['DELETE', 'PUT']) {
+ const method = METHOD.toLowerCase();
+ preflightTest(true, true, METHOD, "*", METHOD, [])
+ preflightTest(true, true, METHOD, "*", method, [])
+ preflightTest(false, true, method, "*", METHOD, [])
+ preflightTest(false, true, method, "*", method, [])
+}
+
+// "PATCH" is NOT upper-cased in both places because it is not listed in
+// https://fetch.spec.whatwg.org/#concept-method-normalize.
+// So Access-Control-Allow-Methods value and init["method"] should match
+// case-sensitively.
+preflightTest(true, true, "PATCH", "*", "PATCH", [])
+preflightTest(false, true, "PATCH", "*", "patch", [])
+preflightTest(false, true, "patch", "*", "PATCH", [])
+preflightTest(true, true, "patch", "*", "patch", [])
+
+// "Authorization" header can't be wildcarded.
+preflightTest(false, false, "*", "*", "POST", ["Authorization", "123"])
+preflightTest(true, false, "*", "*, Authorization", "POST", ["Authorization", "123"])
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js
new file mode 100644
index 00000000000000..a4467a6087b0a3
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js
@@ -0,0 +1,37 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+/* Check preflight is ok if status is ok status (200 to 299)*/
+function corsPreflightStatus(desc, corsUrl, preflightStatus) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var requestInit = {"mode": "cors"};
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&allow_headers=x-force-preflight";
+ urlParameters += "&preflight_status=" + preflightStatus;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ if (200 <= preflightStatus && 299 >= preflightStatus) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ }
+ });
+ }, desc);
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+for (status of [200, 201, 202, 203, 204, 205, 206,
+ 300, 301, 302, 303, 304, 305, 306, 307, 308,
+ 400, 401, 402, 403, 404, 405,
+ 501, 502, 503, 504, 505])
+ corsPreflightStatus("Preflight answered with status " + status, corsUrl, status);
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js
new file mode 100644
index 00000000000000..045422f40b1cdb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js
@@ -0,0 +1,62 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/corspreflight.js
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true);
+corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false);
+corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true);
+corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true);
+corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false);
+corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true);
+corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false);
+corsPreflight("CORS [patcH], server allows", corsUrl, "patcH", true);
+corsPreflight("CORS [patcH], server refuses", corsUrl, "patcH", false);
+corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true);
+corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false);
+corsPreflight("CORS [chicken], server allows", corsUrl, "chicken", true);
+corsPreflight("CORS [chicken], server refuses", corsUrl, "chicken", false);
+
+corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]);
+corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]);
+
+var headers = [
+ ["x-test-header1", "allowedOrRefused"],
+ ["x-test-header2", "allowedOrRefused"],
+ ["X-test-header3", "allowedOrRefused"],
+ ["x-test-header-b", "allowedOrRefused"],
+ ["x-test-header-D", "allowedOrRefused"],
+ ["x-test-header-C", "allowedOrRefused"],
+ ["x-test-header-a", "allowedOrRefused"],
+ ["Content-Type", "allowedOrRefused"],
+];
+var safeHeaders= [
+ ["Accept", "*"],
+ ["Accept-Language", "bzh"],
+ ["Content-Language", "eu"],
+];
+
+corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders);
+corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders);
+corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders);
+corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders);
+
+corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders);
+
+promise_test(async t => {
+ const url = `${corsUrl}?allow_headers=*`;
+ await promise_rejects_js(t, TypeError, fetch(url, {
+ headers: {
+ authorization: 'foobar'
+ }
+ }));
+}, '"authorization" should not be covered by the wildcard symbol');
+
+promise_test(async t => {
+ const url = `${corsUrl}?allow_headers=authorization`;
+ await fetch(url, { headers: {
+ authorization: 'foobar'
+ }});
+}, '"authorization" should be covered by "authorization"');
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js
new file mode 100644
index 00000000000000..2aff3134063c35
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js
@@ -0,0 +1,52 @@
+// META: timeout=long
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) {
+ var url = redirectUrl
+ var urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@");
+
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ promise_test(t => {
+ const result = fetch(url + urlParameters, requestInit)
+ if(locationCredentials === "") {
+ return result;
+ } else {
+ return promise_rejects_js(t, TypeError, result);
+ }
+ }, desc);
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, "");
+
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password");
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js
new file mode 100644
index 00000000000000..50848170d0d415
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js
@@ -0,0 +1,46 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) {
+ var urlBaseParameters = "&redirect_status=" + redirectStatus;
+ var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c");
+ var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]};
+
+ promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ });
+ }, desc + " (preflight after redirection success case)");
+ promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit));
+ });
+ }, desc + " (preflight after redirection failure case)");
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code);
+ corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code);
+ corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code);
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js
new file mode 100644
index 00000000000000..cdf4097d566924
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js
@@ -0,0 +1,42 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) {
+ var uuid_token = token();
+ var url = redirectUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ return promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made");
+ assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect");
+ });
+ });
+ }, desc);
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin);
+ corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null");
+ corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin);
+ corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null");
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html b/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html
new file mode 100644
index 00000000000000..217baa3c46b631
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html b/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html
new file mode 100644
index 00000000000000..d69748ab261b90
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html
@@ -0,0 +1,53 @@
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-worker.html b/test/fixtures/wpt/fetch/api/cors/data-url-worker.html
new file mode 100644
index 00000000000000..13113e62621ac8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/data-url-worker.html
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js b/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js
new file mode 100644
index 00000000000000..18b8f6dfa28a84
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js
@@ -0,0 +1,58 @@
+function headerNames(headers) {
+ let names = [];
+ for (let header of headers) {
+ names.push(header[0].toLowerCase());
+ }
+ return names;
+}
+
+/*
+ Check preflight is done
+ Control if server allows method and headers and check accordingly
+ Check control access headers added by UA (for method and headers)
+*/
+function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) {
+ return promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) {
+ var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&");
+ var urlParameters = "token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "method": method};
+ var requestHeaders = [];
+ if (headers)
+ requestHeaders.push.apply(requestHeaders, headers);
+ if (safeHeaders)
+ requestHeaders.push.apply(requestHeaders, safeHeaders);
+ requestInit["headers"] = requestHeaders;
+
+ if (allowed) {
+ urlParameters += "&allow_methods=" + method + "&control_request_headers";
+ if (headers) {
+ //Make the server allow the headers
+ urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C");
+ }
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ if (headers) {
+ var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(",");
+ for (var i in actualHeaders)
+ actualHeaders[i] = actualHeaders[i].trim();
+ for (var header of headers)
+ assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header);
+
+ let accessControlAllowHeaders = headerNames(headers).sort().join(",");
+ assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value");
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token);
+ } else {
+ assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted")
+ }
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token);
+ });
+ }
+ });
+ }, desc);
+}
diff --git a/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json b/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json
new file mode 100644
index 00000000000000..945dc0f93ba4a3
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json
@@ -0,0 +1,13 @@
+[
+ ["accept", "\""],
+ ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"],
+ ["accept-language", "\u0001"],
+ ["accept-language", "@"],
+ ["authorization", "basics"],
+ ["content-language", "\u0001"],
+ ["content-language", "@"],
+ ["content-type", "text/html"],
+ ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"],
+ ["range", "bytes 0-"],
+ ["test", "hi"]
+]
diff --git a/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html b/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html
new file mode 100644
index 00000000000000..feb9f1f2e5bd3e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/crashtests/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/crashtests/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/crashtests/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html b/test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html
new file mode 100644
index 00000000000000..fa1ad1717f0060
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html
@@ -0,0 +1,11 @@
+
+
diff --git a/test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html b/test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html
new file mode 100644
index 00000000000000..646d3c5f8ce9e6
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html
@@ -0,0 +1,11 @@
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js b/test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js
new file mode 100644
index 00000000000000..1b09925d855f3f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js
@@ -0,0 +1,16 @@
+// META: global=window,worker
+
+'use strict';
+
+promise_test(async t => {
+ const response = await fetch('../resources/huge-response.py');
+ const reader = response.body.getReader();
+ // Read one chunk just to show willing.
+ const { value, done } = await reader.read();
+ assert_false(done, 'there should be some data');
+ assert_greater_than(value.byteLength, 0, 'the chunk should be non-empty');
+ // Wait 2 seconds to give it a chance to crash.
+ await new Promise(resolve => t.step_timeout(resolve, 2000));
+ // If we get here without crashing we passed the test.
+ reader.cancel();
+}, 'fetching a huge cacheable file but not reading it should not crash');
diff --git a/test/fixtures/wpt/fetch/api/crashtests/request.html b/test/fixtures/wpt/fetch/api/crashtests/request.html
new file mode 100644
index 00000000000000..2d21930c3bbc87
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/crashtests/request.html
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/credentials/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/credentials/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..9d1d71eeaed6bf
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/credentials/WEB_FEATURES.yml
@@ -0,0 +1,10 @@
+features:
+# The classifier for "http-authentication" intentionally overlaps with the
+# below classifier for "fetch" because the boundary between the two features is
+# somewhat subjective.
+- name: http-authentication
+ files:
+ - authentication-basic.any.js
+ - authentication-redirection.any.js
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js b/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js
new file mode 100644
index 00000000000000..31ccc3869775fe
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+function basicAuth(desc, user, pass, mode, status) {
+ promise_test(function(test) {
+ var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)};
+ var requestInit = {"credentials": mode, "headers": headers};
+ return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) {
+ assert_equals(resp.status, status, "HTTP status is " + status);
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ });
+ }, desc);
+}
+
+basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200);
+basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200);
+basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200);
+basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401);
diff --git a/test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js b/test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js
new file mode 100644
index 00000000000000..5a15507437808f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const authorizationValue = "Basic " + btoa("user:pass");
+async function getAuthorizationHeaderValue(url)
+{
+ const headers = { "Authorization": authorizationValue};
+ const requestInit = {"headers": headers};
+ const response = await fetch(url, requestInit);
+ return response.text();
+}
+
+promise_test(async test => {
+ const result = await getAuthorizationHeaderValue("/fetch/api/resources/dump-authorization-header.py");
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - no redirection");
+
+promise_test(async test => {
+ result = await getAuthorizationHeaderValue("/fetch/api/resources/redirect.py?location=" + encodeURIComponent("/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+
+ result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - same origin redirection");
+
+promise_test(async (test) => {
+ const result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_ORIGIN + "/fetch/api/resources/dump-authorization-header.py?strip_auth_header=true"));
+ assert_equals(result, "none");
+}, "getAuthorizationHeaderValue - cross origin redirection");
diff --git a/test/fixtures/wpt/fetch/api/credentials/cookies.any.js b/test/fixtures/wpt/fetch/api/credentials/cookies.any.js
new file mode 100644
index 00000000000000..de30e477655c28
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/credentials/cookies.any.js
@@ -0,0 +1,49 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function cookies(desc, credentials1, credentials2 ,cookies) {
+ var url = RESOURCES_DIR + "top.txt"
+ var urlParameters = "";
+ var urlCleanParameters = "";
+ if (cookies) {
+ urlParameters +="?pipe=header(Set-Cookie,";
+ urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)";
+ urlCleanParameters +="?pipe=header(Set-Cookie,";
+ urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)";
+ }
+
+ var requestInit = {"credentials": credentials1}
+ promise_test(function(test){
+ var requestInit = {"credentials": credentials1}
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ //check cookies sent
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2});
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response");
+ if (credentials1 != "omit" && credentials2 != "omit") {
+ assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)");
+ }
+ else {
+ assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)");
+ }
+ //clean cookies
+ return fetch(url + urlCleanParameters, {"credentials": "include"});
+ }).catch(function(e) {
+ return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() {
+ return Promise.reject(e);
+ });
+ });
+ }, desc);
+}
+
+cookies("Include mode: 1 cookie", "include", "include", ["a=1"]);
+cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]);
+cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]);
+cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]);
+cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]);
+cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]);
+cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]);
diff --git a/test/fixtures/wpt/fetch/api/headers/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/headers/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js b/test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js
new file mode 100644
index 00000000000000..cafb780c2c75a9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js
@@ -0,0 +1,266 @@
+// META: title=Headers set-cookie special cases
+// META: global=window,worker
+
+const headerList = [
+ ["set-cookie", "foo=bar"],
+ ["Set-Cookie", "fizz=buzz; domain=example.com"],
+];
+
+const setCookie2HeaderList = [
+ ["set-cookie2", "foo2=bar2"],
+ ["Set-Cookie2", "fizz2=buzz2; domain=example2.com"],
+];
+
+function assert_nested_array_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, "Array length is not equal");
+ for (let i = 0; i < expected.length; i++) {
+ assert_array_equals(actual[i], expected[i]);
+ }
+}
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_equals(
+ headers.get("set-cookie"),
+ "foo=bar, fizz=buzz; domain=example.com",
+ );
+}, "Headers.prototype.get combines set-cookie headers in order");
+
+test(function () {
+ const headers = new Headers(headerList);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz; domain=example.com"],
+ ]);
+}, "Headers iterator does not combine set-cookie headers");
+
+test(function () {
+ const headers = new Headers(setCookie2HeaderList);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers iterator does not special case set-cookie2 headers");
+
+test(function () {
+ const headers = new Headers([...headerList, ...setCookie2HeaderList]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz; domain=example.com"],
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers iterator does not combine set-cookie & set-cookie2 headers");
+
+test(function () {
+ // Values are in non alphabetic order, and the iterator should yield in the
+ // headers in the exact order of the input.
+ const headers = new Headers([
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+}, "Headers iterator preserves set-cookie ordering");
+
+test(
+ function () {
+ const headers = new Headers([
+ ["xylophone-header", "1"],
+ ["best-header", "2"],
+ ["set-cookie", "3"],
+ ["a-cool-header", "4"],
+ ["set-cookie", "5"],
+ ["a-cool-header", "6"],
+ ["best-header", "7"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["a-cool-header", "4, 6"],
+ ["best-header", "2, 7"],
+ ["set-cookie", "3"],
+ ["set-cookie", "5"],
+ ["xylophone-header", "1"],
+ ]);
+ },
+ "Headers iterator preserves per header ordering, but sorts keys alphabetically",
+);
+
+test(
+ function () {
+ const headers = new Headers([
+ ["xylophone-header", "7"],
+ ["best-header", "6"],
+ ["set-cookie", "5"],
+ ["a-cool-header", "4"],
+ ["set-cookie", "3"],
+ ["a-cool-header", "2"],
+ ["best-header", "1"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["a-cool-header", "4, 2"],
+ ["best-header", "6, 1"],
+ ["set-cookie", "5"],
+ ["set-cookie", "3"],
+ ["xylophone-header", "7"],
+ ]);
+ },
+ "Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)",
+);
+
+test(function () {
+ const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]);
+ const iterator = headers[Symbol.iterator]();
+ assert_array_equals(iterator.next().value, ["fizz", "buzz"]);
+ headers.append("Set-Cookie", "a=b");
+ assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
+ headers.append("Accept", "text/html");
+ assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
+ assert_array_equals(iterator.next().value, ["x-header", "test"]);
+ headers.append("set-cookie", "c=d");
+ assert_array_equals(iterator.next().value, ["x-header", "test"]);
+ assert_true(iterator.next().done);
+}, "Headers iterator is correctly updated with set-cookie changes");
+
+test(function () {
+ const headers = new Headers([
+ ["set-cookie", "a"],
+ ["set-cookie", "b"],
+ ["set-cookie", "c"]
+ ]);
+ const iterator = headers[Symbol.iterator]();
+ assert_array_equals(iterator.next().value, ["set-cookie", "a"]);
+ headers.delete("set-cookie");
+ headers.append("set-cookie", "d");
+ headers.append("set-cookie", "e");
+ headers.append("set-cookie", "f");
+ assert_array_equals(iterator.next().value, ["set-cookie", "e"]);
+ assert_array_equals(iterator.next().value, ["set-cookie", "f"]);
+ assert_true(iterator.next().done);
+}, "Headers iterator is correctly updated with set-cookie changes #2");
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_true(headers.has("sEt-cOoKiE"));
+}, "Headers.prototype.has works for set-cookie");
+
+test(function () {
+ const headers = new Headers(setCookie2HeaderList);
+ headers.append("set-Cookie", "foo=bar");
+ headers.append("sEt-cOoKiE", "fizz=buzz");
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz"],
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers.prototype.append works for set-cookie");
+
+test(function () {
+ const headers = new Headers(headerList);
+ headers.set("set-cookie", "foo2=bar2");
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo2=bar2"],
+ ]);
+}, "Headers.prototype.set works for set-cookie");
+
+test(function () {
+ const headers = new Headers(headerList);
+ headers.delete("set-Cookie");
+ const list = [...headers];
+ assert_nested_array_equals(list, []);
+}, "Headers.prototype.delete works for set-cookie");
+
+test(function () {
+ const headers = new Headers();
+ assert_array_equals(headers.getSetCookie(), []);
+}, "Headers.prototype.getSetCookie with no headers present");
+
+test(function () {
+ const headers = new Headers([headerList[0]]);
+ assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
+}, "Headers.prototype.getSetCookie with one header");
+
+test(function () {
+ const headers = new Headers({ "Set-Cookie": "foo=bar" });
+ assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
+}, "Headers.prototype.getSetCookie with one header created from an object");
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_array_equals(headers.getSetCookie(), [
+ "foo=bar",
+ "fizz=buzz; domain=example.com",
+ ]);
+}, "Headers.prototype.getSetCookie with multiple headers");
+
+test(function () {
+ const headers = new Headers([["set-cookie", ""]]);
+ assert_array_equals(headers.getSetCookie(), [""]);
+}, "Headers.prototype.getSetCookie with an empty header");
+
+test(function () {
+ const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]);
+ assert_array_equals(headers.getSetCookie(), ["x", "x"]);
+}, "Headers.prototype.getSetCookie with two equal headers");
+
+test(function () {
+ const headers = new Headers([
+ ["set-cookie2", "x"],
+ ["set-cookie", "y"],
+ ["set-cookie2", "z"],
+ ]);
+ assert_array_equals(headers.getSetCookie(), ["y"]);
+}, "Headers.prototype.getSetCookie ignores set-cookie2 headers");
+
+test(function () {
+ // Values are in non alphabetic order, and the iterator should yield in the
+ // headers in the exact order of the input.
+ const headers = new Headers([
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+ assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]);
+}, "Headers.prototype.getSetCookie preserves header ordering");
+
+test(function () {
+ const headers = new Headers({"Set-Cookie": " a=b\n"});
+ headers.append("set-cookie", "\n\rc=d ");
+ assert_nested_array_equals([...headers], [
+ ["set-cookie", "a=b"],
+ ["set-cookie", "c=d"]
+ ]);
+ headers.set("set-cookie", "\te=f ");
+ assert_nested_array_equals([...headers], [["set-cookie", "e=f"]]);
+}, "Adding Set-Cookie headers normalizes their value");
+
+test(function () {
+ assert_throws_js(TypeError, () => {
+ new Headers({"set-cookie": "\0"});
+ });
+
+ const headers = new Headers();
+ assert_throws_js(TypeError, () => {
+ headers.append("Set-Cookie", "a\nb");
+ });
+ assert_throws_js(TypeError, () => {
+ headers.set("Set-Cookie", "a\rb");
+ });
+}, "Adding invalid Set-Cookie headers throws");
+
+test(function () {
+ const response = new Response();
+ response.headers.append("Set-Cookie", "foo=bar");
+ assert_array_equals(response.headers.getSetCookie(), []);
+ response.headers.append("sEt-cOokIe", "bar=baz");
+ assert_array_equals(response.headers.getSetCookie(), []);
+}, "Set-Cookie is a forbidden response header");
diff --git a/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js b/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js
new file mode 100644
index 00000000000000..ce44fca8213a06
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js
@@ -0,0 +1,73 @@
+// META: title=Header value normalizing test
+// META: global=window,worker
+// META: timeout=long
+
+"use strict";
+
+for(let i = 0; i < 0x21; i++) {
+ let fail = false,
+ strip = false
+
+ // REMOVE 0x0B/0x0C exception once https://github.com/web-platform-tests/wpt/issues/8372 is fixed
+ if(i === 0x0B || i === 0x0C)
+ continue
+
+ if(i === 0) {
+ fail = true
+ }
+
+ if(i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) {
+ strip = true
+ }
+
+ let url = "../resources/inspect-headers.py?headers=val1|val2|val3",
+ val = String.fromCharCode(i),
+ expectedVal = strip ? "" : val,
+ val1 = val,
+ expectedVal1 = expectedVal,
+ val2 = "x" + val,
+ expectedVal2 = "x" + expectedVal,
+ val3 = val + "x",
+ expectedVal3 = expectedVal + "x"
+
+ // XMLHttpRequest is not available in service workers
+ if (!self.GLOBAL.isWorker()) {
+ async_test((t) => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", url)
+ if(fail) {
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val1", val1))
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val2", val2))
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val3", val3))
+ t.done()
+ } else {
+ xhr.setRequestHeader("val1", val1)
+ xhr.setRequestHeader("val2", val2)
+ xhr.setRequestHeader("val3", val3)
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.getResponseHeader("x-request-val1"), expectedVal1)
+ assert_equals(xhr.getResponseHeader("x-request-val2"), expectedVal2)
+ assert_equals(xhr.getResponseHeader("x-request-val3"), expectedVal3)
+ })
+ xhr.onerror = t.unreached_func("XHR should not fail")
+ xhr.send()
+ }
+ }, "XMLHttpRequest with value " + encodeURI(val))
+ }
+
+ promise_test((t) => {
+ if(fail) {
+ return Promise.all([
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val1": val1} })),
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val2": val2} })),
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val3": val3} }))
+ ])
+ } else {
+ return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => {
+ assert_equals(res.headers.get("x-request-val1"), expectedVal1)
+ assert_equals(res.headers.get("x-request-val2"), expectedVal2)
+ assert_equals(res.headers.get("x-request-val3"), expectedVal3)
+ })
+ }
+ }, "fetch() with value " + encodeURI(val))
+}
diff --git a/test/fixtures/wpt/fetch/api/headers/header-values.any.js b/test/fixtures/wpt/fetch/api/headers/header-values.any.js
new file mode 100644
index 00000000000000..78db34f103d40b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/header-values.any.js
@@ -0,0 +1,64 @@
+// META: title=Header value test
+// META: global=window,worker
+// META: timeout=long
+
+"use strict";
+
+// Invalid values
+[0, 0x0A, 0x0D].forEach(val => {
+ val = "x" + String.fromCharCode(val) + "x"
+
+ // XMLHttpRequest is not available in service workers
+ if (!self.GLOBAL.isWorker()) {
+ test(() => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", "/")
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("value-test", val))
+ }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw")
+ }
+
+ promise_test(t => promise_rejects_js(t, TypeError, fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw")
+})
+
+// Valid values
+let headerValues =[]
+for(let i = 0; i < 0x100; i++) {
+ if(i === 0 || i === 0x0A || i === 0x0D) {
+ continue
+ }
+ headerValues.push("x" + String.fromCharCode(i) + "x")
+}
+var url = "../resources/inspect-headers.py?headers="
+headerValues.forEach((_, i) => {
+ url += "val" + i + "|"
+})
+
+// XMLHttpRequest is not available in service workers
+if (!self.GLOBAL.isWorker()) {
+ async_test((t) => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", url)
+ headerValues.forEach((val, i) => {
+ xhr.setRequestHeader("val" + i, val)
+ })
+ xhr.onload = t.step_func_done(() => {
+ headerValues.forEach((val, i) => {
+ assert_equals(xhr.getResponseHeader("x-request-val" + i), val)
+ })
+ })
+ xhr.onerror = t.unreached_func("XHR should not fail")
+ xhr.send()
+ }, "XMLHttpRequest with all valid values")
+}
+
+promise_test((t) => {
+ const headers = new Headers
+ headerValues.forEach((val, i) => {
+ headers.append("val" + i, val)
+ })
+ return fetch(url, { headers }).then((res) => {
+ headerValues.forEach((val, i) => {
+ assert_equals(res.headers.get("x-request-val" + i), val)
+ })
+ })
+}, "fetch() with all valid values")
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js b/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js
new file mode 100644
index 00000000000000..ead1047645a15f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js
@@ -0,0 +1,275 @@
+// META: title=Headers structure
+// META: global=window,worker
+
+"use strict";
+
+test(function() {
+ new Headers();
+}, "Create headers from no parameter");
+
+test(function() {
+ new Headers(undefined);
+}, "Create headers from undefined parameter");
+
+test(function() {
+ new Headers({});
+}, "Create headers from empty object");
+
+var parameters = [null, 1];
+parameters.forEach(function(parameter) {
+ test(function() {
+ assert_throws_js(TypeError, function() { new Headers(parameter) });
+ }, "Create headers with " + parameter + " should throw");
+});
+
+var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3",
+ "name4": null,
+ "name5": undefined,
+ "name6": 1,
+ "Content-Type": "value4"
+};
+
+var headerSeq = [];
+for (var name in headerDict)
+ headerSeq.push([name, headerDict[name]]);
+
+test(function() {
+ var headers = new Headers(headerSeq);
+ for (name in headerDict) {
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+ assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary");
+}, "Create headers with sequence");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict) {
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Create headers with record");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ var headers2 = new Headers(headers);
+ for (name in headerDict) {
+ assert_equals(headers2.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Create headers with existing headers");
+
+test(function() {
+ var headers = new Headers()
+ headers[Symbol.iterator] = function *() {
+ yield ["test", "test"]
+ }
+ var headers2 = new Headers(headers)
+ assert_equals(headers2.get("test"), "test")
+}, "Create headers with existing headers with custom iterator");
+
+test(function() {
+ var headers = new Headers();
+ for (name in headerDict) {
+ headers.append(name, headerDict[name]);
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Check append method");
+
+test(function() {
+ var headers = new Headers();
+ for (name in headerDict) {
+ headers.set(name, headerDict[name]);
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Check set method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict)
+ assert_true(headers.has(name),"headers has name " + name);
+
+ assert_false(headers.has("nameNotInHeaders"),"headers do not have header: nameNotInHeaders");
+}, "Check has method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict) {
+ assert_true(headers.has(name),"headers have a header: " + name);
+ headers.delete(name)
+ assert_true(!headers.has(name),"headers do not have anymore a header: " + name);
+ }
+}, "Check delete method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict)
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+
+ assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value");
+}, "Check get method");
+
+var headerEntriesDict = {"name1": "value1",
+ "Name2": "value2",
+ "name": "value3",
+ "content-Type": "value4",
+ "Content-Typ": "value5",
+ "Content-Types": "value6"
+};
+var sortedHeaderDict = {};
+var headerValues = [];
+var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) {
+ sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value];
+ headerValues.push(headerEntriesDict[value]);
+ return value.toLowerCase();
+}).sort();
+
+var iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
+function checkIteratorProperties(iterator) {
+ var prototype = Object.getPrototypeOf(iterator);
+ assert_equals(Object.getPrototypeOf(prototype), iteratorPrototype);
+
+ var descriptor = Object.getOwnPropertyDescriptor(prototype, "next");
+ assert_true(descriptor.configurable, "configurable");
+ assert_true(descriptor.enumerable, "enumerable");
+ assert_true(descriptor.writable, "writable");
+}
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.keys();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value, key);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const key of headers.keys())
+ assert_true(sortedHeaderKeys.indexOf(key) != -1);
+}, "Check keys method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.values();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value, sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const value of headers.values())
+ assert_true(headerValues.indexOf(value) != -1);
+}, "Check values method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.entries();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value[0], key);
+ assert_equals(entry.value[1], sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const entry of headers.entries())
+ assert_equals(entry[1], sortedHeaderDict[entry[0]]);
+}, "Check entries method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers[Symbol.iterator]();
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value[0], key);
+ assert_equals(entry.value[1], sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+}, "Check Symbol.iterator method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var reference = sortedHeaderKeys[Symbol.iterator]();
+ headers.forEach(function(value, key, container) {
+ assert_equals(headers, container);
+ const entry = reference.next();
+ assert_false(entry.done);
+ assert_equals(key, entry.value);
+ assert_equals(value, sortedHeaderDict[entry.value]);
+ });
+ assert_true(reference.next().done);
+}, "Check forEach method");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ headers.delete("foo");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz"]);
+ assert_array_equals(actualValues, ["0", "1"]);
+}, "Iteration skips elements removed while iterating");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.delete("bar");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "3"]);
+}, "Removing elements already iterated over causes an element to be skipped during iteration");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.append("X-yZ", "4");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"]);
+ assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]);
+}, "Appending a value pair during iteration causes it to be reached during iteration");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.append("abc", "-1");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "1", "2", "3"]);
+}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time");
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js b/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js
new file mode 100644
index 00000000000000..20b8a9d375aaa0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js
@@ -0,0 +1,54 @@
+// META: title=Headers case management
+// META: global=window,worker
+
+"use strict";
+
+var headerDictCase = {"UPPERCASE": "value1",
+ "lowercase": "value2",
+ "mixedCase": "value3",
+ "Content-TYPE": "value4"
+ };
+
+function checkHeadersCase(originalName, headersToCheck, expectedDict) {
+ var lowCaseName = originalName.toLowerCase();
+ var upCaseName = originalName.toUpperCase();
+ var expectedValue = expectedDict[originalName];
+ assert_equals(headersToCheck.get(originalName), expectedValue,
+ "name: " + originalName + " has value: " + expectedValue);
+ assert_equals(headersToCheck.get(lowCaseName), expectedValue,
+ "name: " + lowCaseName + " has value: " + expectedValue);
+ assert_equals(headersToCheck.get(upCaseName), expectedValue,
+ "name: " + upCaseName + " has value: " + expectedValue);
+}
+
+test(function() {
+ var headers = new Headers(headerDictCase);
+ for (const name in headerDictCase)
+ checkHeadersCase(name, headers, headerDictCase)
+}, "Create headers, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase) {
+ headers.append(name, headerDictCase[name]);
+ checkHeadersCase(name, headers, headerDictCase);
+ }
+}, "Check append method, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase) {
+ headers.set(name, headerDictCase[name]);
+ checkHeadersCase(name, headers, headerDictCase);
+ }
+}, "Check set method, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase)
+ headers.set(name, headerDictCase[name]);
+ for (const name in headerDictCase)
+ headers.delete(name.toLowerCase());
+ for (const name in headerDictCase)
+ assert_false(headers.has(name), "header " + name + " should have been deleted");
+}, "Check delete method, names use characters with different case");
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js b/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js
new file mode 100644
index 00000000000000..4f3b6d11df9748
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js
@@ -0,0 +1,66 @@
+// META: title=Headers have combined (and sorted) values
+// META: global=window,worker
+
+"use strict";
+
+var headerSeqCombine = [["single", "singleValue"],
+ ["double", "doubleValue1"],
+ ["double", "doubleValue2"],
+ ["triple", "tripleValue1"],
+ ["triple", "tripleValue2"],
+ ["triple", "tripleValue3"]
+];
+var expectedDict = {"single": "singleValue",
+ "double": "doubleValue1, doubleValue2",
+ "triple": "tripleValue1, tripleValue2, tripleValue3"
+};
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict)
+ assert_equals(headers.get(name), expectedDict[name]);
+}, "Create headers using same name for different values");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ assert_true(headers.has(name), "name: " + name + " has value(s)");
+ headers.delete(name);
+ assert_false(headers.has(name), "name: " + name + " has no value(s) anymore");
+ }
+}, "Check delete and has methods when using same name for different values");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ headers.set(name,"newSingleValue");
+ assert_equals(headers.get(name), "newSingleValue", "name: " + name + " has value: newSingleValue");
+ }
+}, "Check set methods when called with already used name");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ var value = headers.get(name);
+ headers.append(name,"newSingleValue");
+ assert_equals(headers.get(name), (value + ", " + "newSingleValue"));
+ }
+}, "Check append methods when called with already used name");
+
+test(() => {
+ const headers = new Headers([["1", "a"],["1", "b"]]);
+ for(let header of headers) {
+ assert_array_equals(header, ["1", "a, b"]);
+ }
+}, "Iterate combined values");
+
+test(() => {
+ const headers = new Headers([["2", "a"], ["1", "b"], ["2", "b"]]),
+ expected = [["1", "b"], ["2", "a, b"]];
+ let i = 0;
+ for(let header of headers) {
+ assert_array_equals(header, expected[i]);
+ i++;
+ }
+ assert_equals(i, 2);
+}, "Iterate combined values in sorted order")
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js b/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js
new file mode 100644
index 00000000000000..82dadd82340389
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js
@@ -0,0 +1,96 @@
+// META: title=Headers errors
+// META: global=window,worker
+
+"use strict";
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["name"]]); });
+}, "Create headers giving an array having one string as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]); });
+}, "Create headers giving an array having three strings as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["invalidĀ", "Value1"]]); });
+}, "Create headers giving bad header name as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["name", "invalidValueĀ"]]); });
+}, "Create headers giving bad header value as init argument");
+
+var badNames = ["invalidĀ", {}];
+var badValues = ["invalidĀ"];
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.get(name); });
+ }, "Check headers get with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.delete(name); });
+ }, "Check headers delete with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.has(name); });
+ }, "Check headers has with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.set(name, "Value1"); });
+ }, "Check headers set with an invalid name " + name);
+});
+
+badValues.forEach(function(value) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.set("name", value); });
+ }, "Check headers set with an invalid value " + value);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.append("invalidĀ", "Value1"); });
+ }, "Check headers append with an invalid name " + name);
+});
+
+badValues.forEach(function(value) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.append("name", value); });
+ }, "Check headers append with an invalid value " + value);
+});
+
+test(function() {
+ var headers = new Headers([["name", "value"]]);
+ assert_throws_js(TypeError, function() { headers.forEach(); });
+ assert_throws_js(TypeError, function() { headers.forEach(undefined); });
+ assert_throws_js(TypeError, function() { headers.forEach(1); });
+}, "Headers forEach throws if argument is not callable");
+
+test(function() {
+ var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]]);
+ var counter = 0;
+ try {
+ headers.forEach(function(value, name) {
+ counter++;
+ if (name == "name2")
+ throw "error";
+ });
+ } catch (e) {
+ assert_equals(counter, 2);
+ assert_equals(e, "error");
+ return;
+ }
+ assert_unreached();
+}, "Headers forEach loop should stop if callback is throwing exception");
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js b/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js
new file mode 100644
index 00000000000000..60dbb9ef67a479
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js
@@ -0,0 +1,59 @@
+// META: global=window,worker
+
+"use strict";
+
+promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…");
+
+const longValue = "s".repeat(127);
+
+[
+ {
+ "headers": ["accept", "accept-language", "content-language"],
+ "values": [longValue, "", longValue]
+ },
+ {
+ "headers": ["accept", "accept-language", "content-language"],
+ "values": ["", longValue]
+ },
+ {
+ "headers": ["content-type"],
+ "values": ["text/plain;" + "s".repeat(116), "text/plain"]
+ }
+].forEach(testItem => {
+ testItem.headers.forEach(header => {
+ test(() => {
+ const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers;
+ testItem.values.forEach((value) => {
+ noCorsHeaders.append(header, value);
+ assert_equals(noCorsHeaders.get(header), testItem.values[0], '1');
+ });
+ noCorsHeaders.set(header, testItem.values.join(", "));
+ assert_equals(noCorsHeaders.get(header), testItem.values[0], '2');
+ noCorsHeaders.delete(header);
+ assert_false(noCorsHeaders.has(header));
+ }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", "));
+ });
+});
+
+function runTests(testArray) {
+ testArray = testArray.concat([
+ ["dpr", "2"],
+ ["rtt", "1.0"],
+ ["downlink", "-1.0"],
+ ["ect", "6g"],
+ ["save-data", "on"],
+ ["viewport-width", "100"],
+ ["width", "100"],
+ ["unknown", "doesitmatter"]
+ ]);
+ testArray.forEach(testItem => {
+ const [headerName, headerValue] = testItem;
+ test(() => {
+ const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers;
+ noCorsHeaders.append(headerName, headerValue);
+ assert_false(noCorsHeaders.has(headerName));
+ noCorsHeaders.set(headerName, headerValue);
+ assert_false(noCorsHeaders.has(headerName));
+ }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header");
+ });
+}
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js b/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js
new file mode 100644
index 00000000000000..68cf5b85f3acb7
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js
@@ -0,0 +1,56 @@
+// META: title=Headers normalize values
+// META: global=window,worker
+
+"use strict";
+
+const expectations = {
+ "name1": [" space ", "space"],
+ "name2": ["\ttab\t", "tab"],
+ "name3": [" spaceAndTab\t", "spaceAndTab"],
+ "name4": ["\r\n newLine", "newLine"], //obs-fold cases
+ "name5": ["newLine\r\n ", "newLine"],
+ "name6": ["\r\n\tnewLine", "newLine"],
+ "name7": ["\t\f\tnewLine\n", "\f\tnewLine"],
+ "name8": ["newLine\xa0", "newLine\xa0"], // \xa0 == non breaking space
+};
+
+test(function () {
+ const headerDict = Object.fromEntries(
+ Object.entries(expectations).map(([name, [actual]]) => [name, actual]),
+ );
+ var headers = new Headers(headerDict);
+ for (const name in expectations) {
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has normalized value: " + expected,
+ );
+ }
+}, "Create headers with not normalized values");
+
+test(function () {
+ var headers = new Headers();
+ for (const name in expectations) {
+ headers.append(name, expectations[name][0]);
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has value: " + expected,
+ );
+ }
+}, "Check append method with not normalized values");
+
+test(function () {
+ var headers = new Headers();
+ for (const name in expectations) {
+ headers.set(name, expectations[name][0]);
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has value: " + expected,
+ );
+ }
+}, "Check set method with not normalized values");
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-record.any.js b/test/fixtures/wpt/fetch/api/headers/headers-record.any.js
new file mode 100644
index 00000000000000..fa853914f4879f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-record.any.js
@@ -0,0 +1,357 @@
+// META: global=window,worker
+
+"use strict";
+
+var log = [];
+function clearLog() {
+ log = [];
+}
+function addLogEntry(name, args) {
+ log.push([ name, ...args ]);
+}
+
+var loggingHandler = {
+};
+
+setup(function() {
+ for (let prop of Object.getOwnPropertyNames(Reflect)) {
+ loggingHandler[prop] = function(...args) {
+ addLogEntry(prop, args);
+ return Reflect[prop](...args);
+ }
+ }
+});
+
+test(function() {
+ var h = new Headers();
+ assert_equals([...h].length, 0);
+}, "Passing nothing to Headers constructor");
+
+test(function() {
+ var h = new Headers(undefined);
+ assert_equals([...h].length, 0);
+}, "Passing undefined to Headers constructor");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(null);
+ });
+}, "Passing null to Headers constructor");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b" };
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["a"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+}, "Basic operation with one property");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var recordProto = { c: "d" };
+ var record = Object.create(recordProto, { a: { value: "b", enumerable: true } });
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["a"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+}, "Basic operation with one property and a proto");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b", c: "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[5], ["get", record, "c", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 2);
+ assert_array_equals([...h.keys()], ["a", "c"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with two properties");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b", "\uFFFF": "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(proxy);
+ });
+
+ assert_equals(log.length, 5);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "\uFFFF"]);
+ // The second [[Get]] never happens, because we convert the invalid name to a
+ // ByteString first and throw.
+}, "Correct operation ordering with two properties one of which has an invalid name");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "\uFFFF", c: "d" }
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(proxy);
+ });
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Nothing else after this, because converting the result of that [[Get]] to a
+ // ByteString throws.
+}, "Correct operation ordering with two properties one of which has an invalid value");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {};
+ Object.defineProperty(record, "a", { value: "b", enumerable: false });
+ Object.defineProperty(record, "c", { value: "d", enumerable: true });
+ Object.defineProperty(record, "e", { value: "f", enumerable: false });
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // No [[Get]] because not enumerable
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[3], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[4], ["get", record, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "e"]);
+ // No [[Get]] because not enumerable
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["c"]);
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with non-enumerable properties");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {a: "b", c: "d", e: "f"};
+ var lyingHandler = {
+ getOwnPropertyDescriptor: function(target, name) {
+ if (name == "a" || name == "e") {
+ return undefined;
+ }
+ return Reflect.getOwnPropertyDescriptor(target, name);
+ }
+ };
+ var lyingProxy = new Proxy(record, lyingHandler);
+ var proxy = new Proxy(lyingProxy, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", lyingProxy]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", lyingProxy, "a"]);
+ // No [[Get]] because no descriptor
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[3], ["getOwnPropertyDescriptor", lyingProxy, "c"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[4], ["get", lyingProxy, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", lyingProxy, "e"]);
+ // No [[Get]] because no descriptor
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["c"]);
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with undefined descriptors");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {a: "b", c: "d"};
+ var lyingHandler = {
+ ownKeys: function() {
+ return [ "a", "c", "a", "c" ];
+ },
+ };
+ var lyingProxy = new Proxy(record, lyingHandler);
+ var proxy = new Proxy(lyingProxy, loggingHandler);
+
+ // Returning duplicate keys from ownKeys() throws a TypeError.
+ assert_throws_js(TypeError,
+ function() { var h = new Headers(proxy); });
+
+ assert_equals(log.length, 2);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", lyingProxy]);
+}, "Correct operation ordering with repeated keys");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {
+ a: "b",
+ [Symbol.toStringTag]: {
+ // Make sure the ToString conversion of the value happens
+ // after the ToString conversion of the key.
+ toString: function () { addLogEntry("toString", [this]); return "nope"; }
+ },
+ c: "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError,
+ function() { var h = new Headers(proxy); });
+
+ assert_equals(log.length, 7);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[5], ["get", record, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[6], ["getOwnPropertyDescriptor", record,
+ Symbol.toStringTag]);
+ // Then we throw an exception converting the Symbol to a string, before we do
+ // the third [[Get]].
+}, "Basic operation with Symbol keys");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {
+ a: {
+ toString: function() { addLogEntry("toString", [this]); return "b"; }
+ },
+ [Symbol.toStringTag]: {
+ toString: function () { addLogEntry("toString", [this]); return "nope"; }
+ },
+ c: {
+ toString: function() { addLogEntry("toString", [this]); return "d"; }
+ }
+ };
+ // Now make that Symbol-named property not enumerable.
+ Object.defineProperty(record, Symbol.toStringTag, { enumerable: false });
+ assert_array_equals(Reflect.ownKeys(record),
+ ["a", "c", Symbol.toStringTag]);
+
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 9);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the ToString on the value.
+ assert_array_equals(log[4], ["toString", record.a]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[6], ["get", record, "c", proxy]);
+ // Then the ToString on the value.
+ assert_array_equals(log[7], ["toString", record.c]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[8], ["getOwnPropertyDescriptor", record,
+ Symbol.toStringTag]);
+ // No [[Get]] because not enumerable.
+
+ // Check the results.
+ assert_equals([...h].length, 2);
+ assert_array_equals([...h.keys()], ["a", "c"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Operation with non-enumerable Symbol keys");
diff --git a/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js b/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js
new file mode 100644
index 00000000000000..d826bcab2a01f2
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js
@@ -0,0 +1,20 @@
+// META: title=Headers basic
+// META: global=window,worker
+
+"use strict";
+
+var headers = new Headers();
+var methods = ["append",
+ "delete",
+ "get",
+ "has",
+ "set",
+ //Headers is iterable
+ "entries",
+ "keys",
+ "values"
+ ];
+for (var idx in methods)
+ test(function() {
+ assert_true(methods[idx] in headers, "headers has " + methods[idx] + " method");
+ }, "Headers has " + methods[idx] + " method");
diff --git a/test/fixtures/wpt/fetch/api/idlharness.https.any.js b/test/fixtures/wpt/fetch/api/idlharness.https.any.js
new file mode 100644
index 00000000000000..7b3c694e16ac3e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/idlharness.https.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+idl_test(
+ ['fetch'],
+ ['referrer-policy', 'html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ Headers: ["new Headers()"],
+ Request: ["new Request('about:blank')"],
+ Response: ["new Response()"],
+ });
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({ Window: ['window'] });
+ } else if (self.GLOBAL.isWorker()) {
+ idl_array.add_objects({ WorkerGlobalScope: ['self'] });
+ }
+ }
+);
diff --git a/test/fixtures/wpt/fetch/api/policies/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/policies/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html b/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html
new file mode 100644
index 00000000000000..e8660dffa9496d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Fetch in worker: blocked by CSP
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html
new file mode 100644
index 00000000000000..99e90dfcd8fdd7
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Fetch: blocked by CSP
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers
new file mode 100644
index 00000000000000..c8c1e9ffbd9b1c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: connect-src 'none';
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js
new file mode 100644
index 00000000000000..28653fff85cf1d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js
@@ -0,0 +1,13 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+}
+
+//Content-Security-Policy: connect-src 'none'; cf .headers file
+cspViolationUrl = RESOURCES_DIR + "top.txt";
+
+promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(cspViolationUrl));
+}, "Fetch is blocked by CSP, got a TypeError");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers
new file mode 100644
index 00000000000000..c8c1e9ffbd9b1c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers
@@ -0,0 +1 @@
+Content-Security-Policy: connect-src 'none';
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/nested-policy.js b/test/fixtures/wpt/fetch/api/policies/nested-policy.js
new file mode 100644
index 00000000000000..b0d17696c3379c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/nested-policy.js
@@ -0,0 +1 @@
+// empty, but referrer-policy set on this file
diff --git a/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers b/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers
new file mode 100644
index 00000000000000..7ffbf17d6be5a5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html
new file mode 100644
index 00000000000000..af898aa29f5f6e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Fetch in service worker: referrer with no-referrer policy
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html
new file mode 100644
index 00000000000000..dbef9bb658fa67
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Fetch in worker: referrer with no-referrer policy
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html
new file mode 100644
index 00000000000000..22a6f34c525bad
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Fetch: referrer with no-referrer policy
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers
new file mode 100644
index 00000000000000..7ffbf17d6be5a5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js
new file mode 100644
index 00000000000000..60600bf081c71b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js
@@ -0,0 +1,19 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+}
+
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ var referrer = resp.headers.get("x-request-referer");
+ //Either no referrer header is sent or it is empty
+ if (referrer)
+ assert_equals(referrer, "", "request's referrer is empty");
+ });
+}, "Request's referrer is empty");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers
new file mode 100644
index 00000000000000..7ffbf17d6be5a5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html
new file mode 100644
index 00000000000000..4018b837816e66
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Fetch in service worker: referrer with no-referrer policy
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html
new file mode 100644
index 00000000000000..d87192e227119a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Fetch in service worker: referrer with origin-when-cross-origin policy
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html
new file mode 100644
index 00000000000000..f95ae8cf081d13
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Fetch in worker: referrer with origin-when-cross-origin policy
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html
new file mode 100644
index 00000000000000..5cd79e4b536159
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Fetch: referrer with origin-when-cross-origin policy
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers
new file mode 100644
index 00000000000000..ad768e63294149
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin-when-cross-origin
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js
new file mode 100644
index 00000000000000..0adadbc55081f0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js
@@ -0,0 +1,21 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+ importScripts("/common/get-host-info.sub.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerOrigin = location.origin + '/';
+var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Request's referrer is origin");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers
new file mode 100644
index 00000000000000..ad768e63294149
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin-when-cross-origin
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html
new file mode 100644
index 00000000000000..bb80dd54fbf450
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Fetch in worker: referrer with origin policy
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html
new file mode 100644
index 00000000000000..b164afe01de9bb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Fetch: referrer with origin policy
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers
new file mode 100644
index 00000000000000..5b29739bbdde3a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js
new file mode 100644
index 00000000000000..918f8f207c3914
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js
@@ -0,0 +1,30 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerOrigin = (new URL("/", location.href)).href;
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Request's referrer is origin");
+
+promise_test(function(test) {
+ var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/";
+ return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Cross-origin referrer is overridden by client origin");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers
new file mode 100644
index 00000000000000..5b29739bbdde3a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html
new file mode 100644
index 00000000000000..634877edae8764
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Fetch in worker: referrer with unsafe-url policy
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html
new file mode 100644
index 00000000000000..42045776b12027
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Fetch in worker: referrer with unsafe-url policy
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html
new file mode 100644
index 00000000000000..10dd79e3d358b1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Fetch: referrer with unsafe-url policy
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers
new file mode 100644
index 00000000000000..8e23770bd60404
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: unsafe-url
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js
new file mode 100644
index 00000000000000..4d61172613ee58
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js
@@ -0,0 +1,21 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerUrl = location.href;
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl);
+ });
+}, "Request's referrer is the full url of current document/worker");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers
new file mode 100644
index 00000000000000..8e23770bd60404
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: unsafe-url
diff --git a/test/fixtures/wpt/fetch/api/redirect/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/redirect/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js
new file mode 100644
index 00000000000000..74d731f24251c1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const BASE = location.href;
+const IS_HTTPS = new URL(BASE).protocol === 'https:';
+const REMOTE_HOST = get_host_info()['REMOTE_HOST'];
+const REMOTE_PORT =
+ IS_HTTPS ? get_host_info()['HTTPS_PORT'] : get_host_info()['HTTP_PORT'];
+
+const REMOTE_ORIGIN =
+ new URL(`//${REMOTE_HOST}:${REMOTE_PORT}`, BASE).origin;
+const DESTINATION = new URL('../resources/cors-top.txt', BASE);
+
+function CreateURL(url, BASE, params) {
+ const u = new URL(url, BASE);
+ for (const {name, value} of params) {
+ u.searchParams.append(name, value);
+ }
+ return u;
+}
+
+const redirect =
+ CreateURL('/fetch/api/resources/redirect.py', REMOTE_ORIGIN,
+ [{name: 'redirect_status', value: 303},
+ {name: 'location', value: DESTINATION.href}]);
+
+promise_test(async (test) => {
+ const res = await fetch(redirect.href, {mode: 'no-cors'});
+ // This is discussed at https://github.com/whatwg/fetch/issues/737.
+ assert_equals(res.type, 'opaque');
+}, 'original => remote => original with mode: "no-cors"');
+
+promise_test(async (test) => {
+ const res = await fetch(redirect.href, {mode: 'cors'});
+ assert_equals(res.type, 'cors');
+}, 'original => remote => original with mode: "cors"');
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js
new file mode 100644
index 00000000000000..420f9c0dfcb406
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+/**
+ * Fetches a target that returns response with HTTP status code `statusCode` to
+ * redirect `maxCount` times.
+ */
+function redirectCountTest(maxCount, {statusCode, shouldPass = true} = {}) {
+ const desc = `Redirect ${statusCode} ${maxCount} times`;
+
+ const fromUrl = `${RESOURCES_DIR}redirect.py`;
+ const toUrl = fromUrl;
+ const token1 = token();
+ const url = `${fromUrl}?token=${token1}` +
+ `&max_age=0` +
+ `&redirect_status=${statusCode}` +
+ `&max_count=${maxCount}` +
+ `&location=${encodeURIComponent(toUrl)}`;
+
+ const requestInit = {'redirect': 'follow'};
+
+ promise_test((test) => {
+ return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`)
+ .then((resp) => {
+ assert_equals(
+ resp.status, 200, 'Clean stash response\'s status is 200');
+
+ if (!shouldPass)
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+
+ return fetch(url, requestInit)
+ .then((resp) => {
+ assert_equals(resp.status, 200, 'Response\'s status is 200');
+ return resp.text();
+ })
+ .then((body) => {
+ assert_equals(
+ body, maxCount.toString(), `Redirected ${maxCount} times`);
+ });
+ });
+ }, desc);
+}
+
+for (const statusCode of [301, 302, 303, 307, 308]) {
+ redirectCountTest(20, {statusCode});
+ redirectCountTest(21, {statusCode, shouldPass: false});
+}
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js
new file mode 100644
index 00000000000000..487f4d42e9239f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// Tests receiving a redirect response with a Location header with an empty
+// value.
+
+const url = RESOURCES_DIR + 'redirect-empty-location.py';
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'}));
+}, 'redirect response with empty Location, follow mode');
+
+promise_test(t => {
+ return fetch(url, {redirect:'manual'})
+ .then(resp => {
+ assert_equals(resp.type, 'opaqueredirect');
+ assert_equals(resp.status, 0);
+ });
+}, 'redirect response with empty Location, manual mode');
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js
new file mode 100644
index 00000000000000..c9ac13f3dbb27d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js
@@ -0,0 +1,35 @@
+// META: global=window
+// META: timeout=long
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+
+keepaliveRedirectInUnloadTest('same-origin redirect');
+keepaliveRedirectInUnloadTest(
+ 'same-origin redirect + preflight', {withPreflight: true});
+keepaliveRedirectInUnloadTest('cross-origin redirect', {
+ origin1: HTTP_REMOTE_ORIGIN,
+ origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+});
+keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', {
+ origin1: HTTP_REMOTE_ORIGIN,
+ origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ withPreflight: true
+});
+keepaliveRedirectInUnloadTest(
+ 'redirect to file URL',
+ {url2: 'file://tmp/bar.txt', expectFetchSucceed: false});
+keepaliveRedirectInUnloadTest('redirect to data URL', {
+ url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5',
+ expectFetchSucceed: false
+});
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js
new file mode 100644
index 00000000000000..54e4bc31fa1bd0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js
@@ -0,0 +1,18 @@
+// META: global=window
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+keepaliveRedirectTest(`mixed content redirect`, {
+ origin1: HTTPS_NOTSAMESITE_ORIGIN,
+ origin2: HTTP_NOTSAMESITE_ORIGIN,
+ expectFetchSucceed: false
+});
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js
new file mode 100644
index 00000000000000..779ad7057937f6
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js
@@ -0,0 +1,46 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by
+// this test. As of writing, the Fetch spec has not been updated to cover these.
+
+// redirectLocation tests that a Location header of |locationHeader| is resolved
+// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted
+// as a byte sequence via isomorphic encode, as described in [INFRA]. This
+// allows the caller to specify byte sequences which are not valid UTF-8.
+// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its
+// UTF-8 encoding, not "\u2603".
+//
+// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode
+function redirectLocation(
+ desc, redirectUrl, locationHeader, expectedUrlSuffix) {
+ promise_test(function(test) {
+ // Note we use escape() instead of encodeURIComponent(), so that characters
+ // are escaped as bytes in the isomorphic encoding.
+ var url = redirectUrl + '?simple=1&location=' + escape(locationHeader);
+
+ return fetch(url, {'redirect': 'follow'}).then(function(resp) {
+ assert_true(
+ resp.url.endsWith(expectedUrlSuffix),
+ resp.url + ' ends with ' + expectedUrlSuffix);
+ });
+ }, desc);
+}
+
+var redirUrl = RESOURCES_DIR + 'redirect.py';
+redirectLocation(
+ 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83',
+ 'top.txt?%E2%98%83%e2%98%83');
+redirectLocation(
+ 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83',
+ 'top.txt?%E2%98%83');
+redirectLocation(
+ 'Redirect to escaped and unescaped UTF-8', redirUrl,
+ 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83');
+redirectLocation(
+ 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83',
+ 'top.txt?%%E2%98%83');
+redirectLocation(
+ 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF');
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js
new file mode 100644
index 00000000000000..3d483bdcd49e36
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js
@@ -0,0 +1,73 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+const VALID_URL = 'top.txt';
+const INVALID_URL = 'invalidurl:';
+const DATA_URL = 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5';
+
+/**
+ * A test to fetch a URL that returns response redirecting to `toUrl` with
+ * `status` as its HTTP status code. `expectStatus` can be set to test the
+ * status code in fetch's Promise response.
+ */
+function redirectLocationTest(toUrlDesc, {
+ toUrl = undefined,
+ status,
+ expectStatus = undefined,
+ mode,
+ shouldPass = true
+} = {}) {
+ toUrlDesc = toUrl ? `with ${toUrlDesc}` : `without`;
+ const desc = `Redirect ${status} in "${mode}" mode ${toUrlDesc} location`;
+ const url = `${RESOURCES_DIR}redirect.py?redirect_status=${status}` +
+ (toUrl ? `&location=${encodeURIComponent(toUrl)}` : '');
+ const requestInit = {'redirect': mode};
+ if (!expectStatus)
+ expectStatus = status;
+
+ promise_test((test) => {
+ if (mode === 'error' || !shouldPass)
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+ if (mode === 'manual')
+ return fetch(url, requestInit).then((resp) => {
+ assert_equals(resp.status, 0, "Response's status is 0");
+ assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+ assert_equals(resp.statusText, '', `Response's statusText is ""`);
+ assert_true(resp.headers.entries().next().done, "Headers should be empty");
+ });
+
+ if (mode === 'follow')
+ return fetch(url, requestInit).then((resp) => {
+ assert_equals(
+ resp.status, expectStatus, `Response's status is ${expectStatus}`);
+ });
+ assert_unreached(`${mode} is not a valid redirect mode`);
+ }, desc);
+}
+
+// FIXME: We may want to mix redirect-mode and cors-mode.
+for (const status of [301, 302, 303, 307, 308]) {
+ redirectLocationTest('without location', {status, mode: 'follow'});
+ redirectLocationTest('without location', {status, mode: 'manual'});
+ // FIXME: Add tests for "error" redirect-mode without location.
+
+ // When succeeded, `follow` mode should have followed all redirects.
+ redirectLocationTest(
+ 'valid', {toUrl: VALID_URL, status, expectStatus: 200, mode: 'follow'});
+ redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'manual'});
+ redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'error'});
+
+ redirectLocationTest(
+ 'invalid',
+ {toUrl: INVALID_URL, status, mode: 'follow', shouldPass: false});
+ redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'manual'});
+ redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'error'});
+
+ redirectLocationTest(
+ 'data', {toUrl: DATA_URL, status, mode: 'follow', shouldPass: false});
+ // FIXME: Should this pass?
+ redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'manual'});
+ redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'error'});
+}
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js
new file mode 100644
index 00000000000000..9fe086a9db718a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js
@@ -0,0 +1,112 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// Creates a promise_test that fetches a URL that returns a redirect response.
+//
+// |opts| has additional options:
+// |opts.body|: the request body as a string or blob (default is empty body)
+// |opts.expectedBodyAsString|: the expected response body as a string. The
+// server is expected to echo the request body. The default is the empty string
+// if the request after redirection isn't POST; otherwise it's |opts.body|.
+// |opts.expectedRequestContentType|: the expected Content-Type of redirected
+// request.
+function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) {
+ let url = redirectUrl;
+ let urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ let requestHeaders = {
+ "Content-Encoding": "Identity",
+ "Content-Language": "en-US",
+ "Content-Location": "foo",
+ };
+ let requestInit = {"method": method, "redirect": "follow", "headers" : requestHeaders};
+ opts = opts || {};
+ if (opts.body) {
+ requestInit.body = opts.body;
+ }
+
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ let expectedRequestContentType = "NO";
+ if (opts.expectedRequestContentType) {
+ expectedRequestContentType = opts.expectedRequestContentType;
+ }
+
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.type, "basic", "Response's type basic");
+ assert_equals(
+ resp.headers.get("x-request-method"),
+ expectedMethod,
+ "Request method after redirection is " + expectedMethod);
+ let hasRequestBodyHeader = true;
+ if (opts.expectedStripRequestBodyHeader) {
+ hasRequestBodyHeader = !opts.expectedStripRequestBodyHeader;
+ }
+ assert_equals(
+ resp.headers.get("x-request-content-type"),
+ expectedRequestContentType,
+ "Request Content-Type after redirection is " + expectedRequestContentType);
+ [
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Location"
+ ].forEach(header => {
+ let xHeader = "x-request-" + header.toLowerCase();
+ let expectedValue = hasRequestBodyHeader ? requestHeaders[header] : "NO";
+ assert_equals(
+ resp.headers.get(xHeader),
+ expectedValue,
+ "Request " + header + " after redirection is " + expectedValue);
+ });
+ assert_true(resp.redirected);
+ return resp.text().then(function(text) {
+ let expectedBody = "";
+ if (expectedMethod == "POST") {
+ expectedBody = opts.expectedBodyAsString || requestInit.body;
+ }
+ let expectedContentLength = expectedBody ? expectedBody.length.toString() : "NO";
+ assert_equals(text, expectedBody, "request body");
+ assert_equals(
+ resp.headers.get("x-request-content-length"),
+ expectedContentLength,
+ "Request Content-Length after redirection is " + expectedContentLength);
+ });
+ });
+ }, desc);
+}
+
+promise_test(function(test) {
+ assert_false(new Response().redirected);
+ return fetch(RESOURCES_DIR + "method.py").then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_false(resp.redirected);
+ });
+}, "Response.redirected should be false on not-redirected responses");
+
+var redirUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = "method.py";
+
+const stringBody = "this is my body";
+const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]);
+const blobBodyAsString = "it's me the blob! and more blob!";
+
+redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET");
+redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD");
+
+redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET");
+redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD");
+
+redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET");
+redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD");
+redirectMethod("Redirect 303 with TESTING", redirUrl, locationUrl, 303, "TESTING", "GET", { expectedStripRequestBodyHeader: true });
+
+redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET");
+redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody , expectedRequestContentType: "text/plain;charset=UTF-8"});
+redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString });
+redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js
new file mode 100644
index 00000000000000..9f1ff98c65af97
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js
@@ -0,0 +1,59 @@
+// META: script=/common/get-host-info.sub.js
+
+var redirectLocation = "cors-top.txt";
+const { ORIGIN, REMOTE_ORIGIN } = get_host_info();
+
+function testRedirect(origin, redirectStatus, redirectMode, corsMode) {
+ var url = new URL("../resources/redirect.py", self.location);
+ if (origin === "cross-origin") {
+ url.host = get_host_info().REMOTE_HOST;
+ url.port = get_host_info().HTTP_PORT;
+ }
+
+ var urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {redirect: redirectMode, mode: corsMode};
+
+ promise_test(function(test) {
+ if (redirectMode === "error" ||
+ (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin"))
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ if (redirectMode === "manual")
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 0, "Response's status is 0");
+ assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+ assert_equals(resp.statusText, "", "Response's statusText is \"\"");
+ assert_equals(resp.url, url + urlParameters, "Response URL should be the original one");
+ });
+ if (redirectMode === "follow")
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ if (corsMode !== "no-cors" || origin === "same-origin") {
+ assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one");
+ assert_equals(resp.status, 200, "Response's status is 200");
+ } else {
+ assert_equals(resp.type, "opaque", "Response is opaque");
+ }
+ });
+ assert_unreached(redirectMode + " is no a valid redirect mode");
+ }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode");
+}
+
+for (var origin of ["same-origin", "cross-origin"]) {
+ for (var statusCode of [301, 302, 303, 307, 308]) {
+ for (var redirect of ["error", "manual", "follow"]) {
+ for (var mode of ["cors", "no-cors"])
+ testRedirect(origin, statusCode, redirect, mode);
+ }
+ }
+}
+
+promise_test(async (t) => {
+ const destination = `${ORIGIN}/common/blank.html`;
+ // We use /common/redirect.py intentionally, as we want a CORS error.
+ const url =
+ `${REMOTE_ORIGIN}/common/redirect.py?location=${destination}`;
+ await promise_rejects_js(t, TypeError, fetch(url, { redirect: "manual" }));
+}, "manual redirect with a CORS error should be rejected");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js
new file mode 100644
index 00000000000000..6001c509b1d125
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js
@@ -0,0 +1,68 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const {
+ HTTP_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+} = get_host_info();
+
+/**
+ * Fetches `fromUrl` with 'cors' and 'follow' modes that returns response to
+ * redirect to `toUrl`.
+ */
+function testOriginAfterRedirection(
+ desc, method, fromUrl, toUrl, statusCode, expectedOrigin) {
+ desc = `[${method}] Redirect ${statusCode} ${desc}`;
+ const token1 = token();
+ const url = `${fromUrl}?token=${token1}&max_age=0` +
+ `&redirect_status=${statusCode}` +
+ `&location=${encodeURIComponent(toUrl)}`;
+
+ const requestInit = {method, 'mode': 'cors', 'redirect': 'follow'};
+
+ promise_test(function(test) {
+ return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`)
+ .then((cleanResponse) => {
+ assert_equals(
+ cleanResponse.status, 200,
+ `Clean stash response's status is 200`);
+ return fetch(url, requestInit).then((redirectResponse) => {
+ assert_equals(
+ redirectResponse.status, 200,
+ `Inspect header response's status is 200`);
+ assert_equals(
+ redirectResponse.headers.get('x-request-origin'),
+ expectedOrigin, 'Check origin header');
+ });
+ });
+ }, desc);
+}
+
+const FROM_URL = `${RESOURCES_DIR}redirect.py`;
+const CORS_FROM_URL =
+ `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${FROM_URL}`;
+const TO_URL = `${HTTP_ORIGIN}${dirname(location.pathname)}${
+ RESOURCES_DIR}inspect-headers.py?headers=origin`;
+const CORS_TO_URL = `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${
+ RESOURCES_DIR}inspect-headers.py?cors&headers=origin`;
+
+for (const statusCode of [301, 302, 303, 307, 308]) {
+ for (const method of ['GET', 'POST']) {
+ testOriginAfterRedirection(
+ 'Same origin to same origin', method, FROM_URL, TO_URL, statusCode,
+ null);
+ testOriginAfterRedirection(
+ 'Same origin to other origin', method, FROM_URL, CORS_TO_URL,
+ statusCode, HTTP_ORIGIN);
+ testOriginAfterRedirection(
+ 'Other origin to other origin', method, CORS_FROM_URL, CORS_TO_URL,
+ statusCode, HTTP_ORIGIN);
+ // TODO(crbug.com/1432059): Fix broken tests.
+ testOriginAfterRedirection(
+ 'Other origin to same origin', method, CORS_FROM_URL, `${TO_URL}&cors`,
+ statusCode, 'null');
+ }
+}
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js
new file mode 100644
index 00000000000000..337f8dd06983e1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js
@@ -0,0 +1,104 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function getExpectation(expectations, initPolicy, initScenario, redirectPolicy, redirectScenario) {
+ let policies = [
+ expectations[initPolicy][initScenario],
+ expectations[redirectPolicy][redirectScenario]
+ ];
+
+ if (policies.includes("omitted")) {
+ return null;
+ } else if (policies.includes("origin")) {
+ return referrerOrigin;
+ } else {
+ // "stripped-referrer"
+ return referrerUrl;
+ }
+}
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+ var url = redirectUrl;
+ var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+ var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header ";
+
+ if (redirectReferrerPolicy)
+ urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+ var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(response) {
+ assert_equals(response.status, 200, "Inspect header response's status is 200");
+ assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+ });
+ }, description);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+var expectations = {
+ "no-referrer": {
+ "same-origin": "omitted",
+ "cross-origin": "omitted"
+ },
+ "no-referrer-when-downgrade": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "stripped-referrer"
+ },
+ "origin": {
+ "same-origin": "origin",
+ "cross-origin": "origin"
+ },
+ "origin-when-cross-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "origin",
+ },
+ "same-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "omitted"
+ },
+ "strict-origin": {
+ "same-origin": "origin",
+ "cross-origin": "origin"
+ },
+ "strict-origin-when-cross-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "origin"
+ },
+ "unsafe-url": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "stripped-referrer"
+ }
+};
+
+for (var initPolicy in expectations) {
+ for (var redirectPolicy in expectations) {
+
+ // Redirect to same-origin URL
+ testReferrerAfterRedirection(
+ "Same origin redirection",
+ redirectUrl,
+ locationUrl,
+ initPolicy,
+ redirectPolicy,
+ getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin"));
+
+ // Redirect to cross-origin URL
+ testReferrerAfterRedirection(
+ "Cross origin redirection",
+ redirectUrl,
+ crossLocationUrl,
+ initPolicy,
+ redirectPolicy,
+ getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin"));
+ }
+}
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js
new file mode 100644
index 00000000000000..99fda42e69b29f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js
@@ -0,0 +1,66 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+ var url = redirectUrl;
+ var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+
+ if (redirectReferrerPolicy)
+ urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+ var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(response) {
+ assert_equals(response.status, 200, "Inspect header response's status is 200");
+ assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+ });
+ }, desc);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl);
+
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl);
+
+testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin);
+
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin);
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js
new file mode 100644
index 00000000000000..31ec124fd6a3ed
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js
@@ -0,0 +1,19 @@
+// META: title=Fetch: handling different schemes in redirects
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+// All non-HTTP(S) schemes cannot survive redirects
+var url = "../resources/redirect.py?location=";
+var tests = [
+ url + "mailto:a@a.com",
+ url + "data:,HI",
+ url + "facetime:a@a.org",
+ url + "about:blank",
+ url + "about:unicorn",
+ url + "blob:djfksfjs"
+];
+tests.forEach(function(url) {
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url))
+ })
+})
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js
new file mode 100644
index 00000000000000..9d0f147349c488
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js
@@ -0,0 +1,28 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5";
+var body = "response's body";
+var contentType = "text/plain";
+
+function redirectDataURL(desc, redirectUrl, mode) {
+ var url = redirectUrl + "?cors&location=" + encodeURIComponent(dataURL);
+
+ var requestInit = {"mode": mode};
+
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+ }, desc);
+}
+
+var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py";
+var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py";
+
+redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin");
+
+redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors");
+redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors");
+
+done();
diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js
new file mode 100644
index 00000000000000..521bd3adc28bff
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js
@@ -0,0 +1,33 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const redirectUrl = RESOURCES_DIR + "redirect.h2.py";
+const redirectLocation = "top.txt";
+
+async function fetchStreamRedirect(statusCode) {
+ const url = RESOURCES_DIR + "redirect.h2.py" +
+ `?redirect_status=${statusCode}&location=${redirectLocation}`;
+ const requestInit = {method: "POST"};
+ requestInit["body"] = new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }});
+ requestInit.duplex = "half";
+ return fetch(url, requestInit);
+}
+
+promise_test(async () => {
+ const resp = await fetchStreamRedirect(303);
+ assert_equals(resp.status, 200);
+ assert_true(new URL(resp.url).pathname.endsWith(redirectLocation),
+ "Response's url should be the redirected one");
+}, "Fetch upload streaming should be accepted on 303");
+
+for (const statusCode of [301, 302, 307, 308]) {
+ promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetchStreamRedirect(statusCode));
+ }, `Fetch upload streaming should fail on ${statusCode}`);
+}
diff --git a/test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..ec4b764e00b8a2
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml
@@ -0,0 +1,8 @@
+features:
+- name: fetch
+ files:
+ - "*"
+ - "!request-init-priority.any.js"
+- name: fetch-priority
+ files:
+ - request-init-priority.any.js
diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html
new file mode 100644
index 00000000000000..f3f9f7856d5d90
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html
@@ -0,0 +1,51 @@
+
+Fetch destination tests for resources with no load event
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html
new file mode 100644
index 00000000000000..1aa5a5613b1c6e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html
@@ -0,0 +1,51 @@
+
+Fetch destination tests for resources with no load event
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html
new file mode 100644
index 00000000000000..2fb4aaebc04822
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html
@@ -0,0 +1,138 @@
+
+Fetch destination tests for resources with no load event
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html
new file mode 100644
index 00000000000000..db99202df87af6
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html
@@ -0,0 +1,46 @@
+
+Fetch destination test for prefetching
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html
new file mode 100644
index 00000000000000..5935c1ff31ec4b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html
@@ -0,0 +1,60 @@
+
+Fetch destination tests for resources with no load event
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html
new file mode 100644
index 00000000000000..1b6cf16914116b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html
@@ -0,0 +1,485 @@
+
+Fetch destination tests
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.css b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.css
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers
new file mode 100644
index 00000000000000..9bb8badcad45ab
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers
@@ -0,0 +1 @@
+Content-Type: text/event-stream
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json
new file mode 100644
index 00000000000000..0967ef424bce67
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json
@@ -0,0 +1 @@
+{}
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png
new file mode 100644
index 00000000000000..01c9666a8de9d5
Binary files /dev/null and b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png differ
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf
new file mode 100644
index 00000000000000..9023592ef5aa83
Binary files /dev/null and b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf differ
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3
new file mode 100644
index 00000000000000..0091330f1ecd9b
Binary files /dev/null and b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 differ
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga
new file mode 100644
index 00000000000000..239ad2bd08c21c
Binary files /dev/null and b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga differ
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4
new file mode 100644
index 00000000000000..7022e75c15ee52
Binary files /dev/null and b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 differ
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm
new file mode 100644
index 00000000000000..c3d433a3e02e86
Binary files /dev/null and b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm differ
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/empty.https.html b/test/fixtures/wpt/fetch/api/request/destination/resources/empty.https.html
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js
new file mode 100644
index 00000000000000..b69de0b7df91ac
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ event.waitUntil(async function() {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ let clients = await self.clients.matchAll({"includeUncontrolled": true});
+ clients.forEach(function(client) {
+ if (client.url.includes("fetch-destination-frame")) {
+ if (event.request.destination == destination) {
+ client.postMessage("PASS");
+ } else {
+ client.postMessage("FAIL");
+ }
+ }
+ })
+ }());
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js
new file mode 100644
index 00000000000000..76345839eadfeb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ event.waitUntil(async function() {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ let clients = await self.clients.matchAll({"includeUncontrolled": true});
+ clients.forEach(function(client) {
+ if (client.url.includes("fetch-destination-iframe")) {
+ if (event.request.destination == destination) {
+ client.postMessage("PASS");
+ } else {
+ client.postMessage("FAIL");
+ }
+ }
+ })
+ }());
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
new file mode 100644
index 00000000000000..a583b1272a128c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ const url = event.request.url;
+ if (url.includes('dummy') && url.includes('?')) {
+ event.waitUntil(async function() {
+ let destination = new URL(url).searchParams.get("dest");
+ var result = "FAIL";
+ if (event.request.destination == destination ||
+ (event.request.destination == "empty" && destination == "")) {
+ result = "PASS";
+ }
+ let cl = await clients.matchAll({includeUncontrolled: true});
+ for (i = 0; i < cl.length; i++) {
+ cl[i].postMessage(result);
+ }
+ }())
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js
new file mode 100644
index 00000000000000..904009c1721645
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ if (event.request.destination == destination ||
+ (event.request.destination == "empty" && destination == "")) {
+ event.respondWith(fetch(event.request));
+ } else {
+ event.respondWith(Response.error());
+ }
+ }
+});
+
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js b/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js
new file mode 100644
index 00000000000000..3c8cf1f44b7157
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js
@@ -0,0 +1 @@
+import "./dummy.css?dest=style" with { type: "css" };
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js b/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js
new file mode 100644
index 00000000000000..b2d964dd824053
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js
@@ -0,0 +1 @@
+import "./dummy.json?dest=json" with { type: "json" };
diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/importer.js b/test/fixtures/wpt/fetch/api/request/destination/resources/importer.js
new file mode 100644
index 00000000000000..9568474d505d09
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/destination/resources/importer.js
@@ -0,0 +1 @@
+importScripts("dummy?t=importScripts&dest=script");
diff --git a/test/fixtures/wpt/fetch/api/request/forbidden-method.any.js b/test/fixtures/wpt/fetch/api/request/forbidden-method.any.js
new file mode 100644
index 00000000000000..eb13f37f0b5ef8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/forbidden-method.any.js
@@ -0,0 +1,13 @@
+// META: global=window,worker
+
+// https://fetch.spec.whatwg.org/#forbidden-method
+for (const method of [
+ 'CONNECT', 'TRACE', 'TRACK',
+ 'connect', 'trace', 'track'
+ ]) {
+ test(function() {
+ assert_throws_js(TypeError,
+ function() { new Request('./', {method: method}); }
+ );
+ }, 'Request() with a forbidden method ' + method + ' must throw.');
+}
diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js b/test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js
new file mode 100644
index 00000000000000..b0d6ba5b80db82
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js
@@ -0,0 +1,11 @@
+// This is a regression test for Chromium issue https://crbug.com/1427266.
+test(() => {
+ const iframe = document.createElement('iframe');
+ document.body.append(iframe);
+ const otherRequest = iframe.contentWindow.Request;
+ iframe.remove();
+ const r1 = new otherRequest('resource', { method: 'POST', body: 'string' });
+ const r2 = new otherRequest(r1);
+ assert_true(r1.bodyUsed);
+ assert_false(r2.bodyUsed);
+}, 'creating a request from another request in a detached realm should work');
diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html b/test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html
new file mode 100644
index 00000000000000..9bb6e0bbf3f8eb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html
@@ -0,0 +1,3 @@
+
+Current page used as a test helper
+
diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html b/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html
new file mode 100644
index 00000000000000..a885b8a0a734b2
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html
@@ -0,0 +1,14 @@
+
+Incumbent page used as a test helper
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html b/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html
new file mode 100644
index 00000000000000..df60e72507ffdf
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html
@@ -0,0 +1,27 @@
+
+Request constructor URL parsing, with multiple globals in play
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js b/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js
new file mode 100644
index 00000000000000..ff394095f64cf5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js
@@ -0,0 +1,95 @@
+// META: global=window,worker
+
+// list of bad ports according to
+// https://fetch.spec.whatwg.org/#port-blocking
+var BLOCKED_PORTS_LIST = [
+ 0,
+ 1, // tcpmux
+ 7, // echo
+ 9, // discard
+ 11, // systat
+ 13, // daytime
+ 15, // netstat
+ 17, // qotd
+ 19, // chargen
+ 20, // ftp-data
+ 21, // ftp
+ 22, // ssh
+ 23, // telnet
+ 25, // smtp
+ 37, // time
+ 42, // name
+ 43, // nicname
+ 53, // domain
+ 69, // tftp
+ 77, // priv-rjs
+ 79, // finger
+ 87, // ttylink
+ 95, // supdup
+ 101, // hostriame
+ 102, // iso-tsap
+ 103, // gppitnp
+ 104, // acr-nema
+ 109, // pop2
+ 110, // pop3
+ 111, // sunrpc
+ 113, // auth
+ 115, // sftp
+ 117, // uucp-path
+ 119, // nntp
+ 123, // ntp
+ 135, // loc-srv / epmap
+ 137, // netbios-ns
+ 139, // netbios-ssn
+ 143, // imap2
+ 161, // snmp
+ 179, // bgp
+ 389, // ldap
+ 427, // afp (alternate)
+ 465, // smtp (alternate)
+ 512, // print / exec
+ 513, // login
+ 514, // shell
+ 515, // printer
+ 526, // tempo
+ 530, // courier
+ 531, // chat
+ 532, // netnews
+ 540, // uucp
+ 548, // afp
+ 554, // rtsp
+ 556, // remotefs
+ 563, // nntp+ssl
+ 587, // smtp (outgoing)
+ 601, // syslog-conn
+ 636, // ldap+ssl
+ 989, // ftps-data
+ 990, // ftps
+ 993, // ldap+ssl
+ 995, // pop3+ssl
+ 1719, // h323gatestat
+ 1720, // h323hostcall
+ 1723, // pptp
+ 2049, // nfs
+ 3659, // apple-sasl
+ 4045, // lockd
+ 4190, // sieve
+ 5060, // sip
+ 5061, // sips
+ 6000, // x11
+ 6566, // sane-port
+ 6665, // irc (alternate)
+ 6666, // irc (alternate)
+ 6667, // irc (default)
+ 6668, // irc (alternate)
+ 6669, // irc (alternate)
+ 6679, // osaut
+ 6697, // irc+tls
+ 10080, // amanda
+];
+
+BLOCKED_PORTS_LIST.map(function(a){
+ promise_test(function(t){
+ return promise_rejects_js(t, TypeError, fetch(`${location.origin}:${a}`))
+ }, 'Request on bad port ' + a + ' should throw TypeError.');
+});
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js
new file mode 100644
index 00000000000000..c5b2001cc8f6b0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js
@@ -0,0 +1,170 @@
+// META: global=window,worker
+// META: title=Request cache - default with conditional requests
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Modified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Modified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Modified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Modified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-None-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-None-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-None-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-None-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Range": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Range": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Range": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Range": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js
new file mode 100644
index 00000000000000..dfa8369c9a3719
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js
@@ -0,0 +1,39 @@
+// META: global=window,worker
+// META: title=Request cache - default
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses',
+ state: "stale",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache',
+ state: "stale",
+ cache_control: "no-store",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache',
+ state: "fresh",
+ cache_control: "no-store",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js
new file mode 100644
index 00000000000000..00dce096c72924
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js
@@ -0,0 +1,67 @@
+// META: global=window,worker
+// META: title=Request cache - force-cache
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses',
+ state: "stale",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses',
+ state: "fresh",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found',
+ state: "stale",
+ request_cache: ["force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found',
+ state: "fresh",
+ request_cache: ["force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary',
+ state: "stale",
+ vary: "*",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary',
+ state: "fresh",
+ vary: "*",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network',
+ state: "stale",
+ request_cache: ["force-cache", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network',
+ state: "fresh",
+ request_cache: ["force-cache", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js
new file mode 100644
index 00000000000000..41fc22baf23ddd
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js
@@ -0,0 +1,25 @@
+// META: global=window,worker
+// META: title=Request cache : no-cache
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache',
+ state: "stale",
+ request_cache: ["default", "no-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ expected_max_age_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache',
+ state: "fresh",
+ request_cache: ["default", "no-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ expected_max_age_headers: [false, true],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js
new file mode 100644
index 00000000000000..9a28718bf2292d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js
@@ -0,0 +1,37 @@
+// META: global=window,worker
+// META: title=Request cache - no store
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "stale",
+ request_cache: ["default", "no-store"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "fresh",
+ request_cache: ["default", "no-store"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not store the response in the cache',
+ state: "stale",
+ request_cache: ["no-store", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not store the response in the cache',
+ state: "fresh",
+ request_cache: ["no-store", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js
new file mode 100644
index 00000000000000..1305787c7c1d66
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js
@@ -0,0 +1,66 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: title=Request cache - only-if-cached
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+// FIXME: avoid mixed content requests to enable service worker global
+var tests = [
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false]
+ },
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false]
+ },
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found',
+ state: "fresh",
+ request_cache: ["only-if-cached"],
+ response: ["error"],
+ expected_validation_headers: [],
+ expected_no_cache_headers: []
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "same-origin",
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "same-origin",
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "cross-origin",
+ response: [null, "error"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "cross-origin",
+ response: [null, "error"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js
new file mode 100644
index 00000000000000..c7bfffb398890d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker
+// META: title=Request cache - reload
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "stale",
+ request_cache: ["default", "reload"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "fresh",
+ request_cache: ["default", "reload"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache',
+ state: "stale",
+ request_cache: ["reload", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache',
+ state: "fresh",
+ request_cache: ["reload", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [true],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored',
+ state: "stale",
+ request_cache: ["default", "reload", "default"],
+ expected_validation_headers: [false, false, true],
+ expected_no_cache_headers: [false, true, false],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored',
+ state: "fresh",
+ request_cache: ["default", "reload", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+];
+run_tests(tests);
diff --git a/test/fixtures/wpt/fetch/api/request/request-cache.js b/test/fixtures/wpt/fetch/api/request/request-cache.js
new file mode 100644
index 00000000000000..f2fbecf4969291
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-cache.js
@@ -0,0 +1,223 @@
+/**
+ * Each test is run twice: once using etag/If-None-Match and once with
+ * date/If-Modified-Since. Each test run gets its own URL and randomized
+ * content and operates independently.
+ *
+ * The test steps are run with request_cache.length fetch requests issued
+ * and their immediate results sanity-checked. The cache.py server script
+ * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma,
+ * and Cache-Control observed headers for each request it receives. When
+ * the test fetches have run, this state is retrieved from cache.py and the
+ * expected_* lists are checked, including their length.
+ *
+ * This means that if a request_* fetch is expected to hit the cache and not
+ * touch the network, then there will be no entry for it in the expect_*
+ * lists. AKA (request_cache.length - expected_validation_headers.length)
+ * should equal the number of cache hits that didn't touch the network.
+ *
+ * Test dictionary keys:
+ * - state: required string that determines whether the Expires response for
+ * the fetched document should be set in the future ("fresh") or past
+ * ("stale").
+ * - vary: optional string to be passed to the server for it to quote back
+ * in a Vary header on the response to us.
+ * - cache_control: optional string to be passed to the server for it to
+ * quote back in a Cache-Control header on the response to us.
+ * - redirect: optional string "same-origin" or "cross-origin". If
+ * provided, the server will issue an absolute redirect to the script on
+ * the same or a different origin, as appropriate. The redirected
+ * location is the script with the redirect parameter removed, so the
+ * content/state/etc. will be as if you hadn't specified a redirect.
+ * - request_cache: required array of cache modes to use (via `cache`).
+ * - request_headers: optional array of explicit fetch `headers` arguments.
+ * If provided, the server will log an empty dictionary for each request
+ * instead of the request headers it would normally log.
+ * - response: optional array of specialized response handling. Right now,
+ * "error" array entries indicate a network error response is expected
+ * which will reject with a TypeError.
+ * - expected_validation_headers: required boolean array indicating whether
+ * the server should have seen an If-None-Match/If-Modified-Since header
+ * in the request.
+ * - expected_no_cache_headers: required boolean array indicating whether
+ * the server should have seen Pragma/Cache-control:no-cache headers in
+ * the request.
+ * - expected_max_age_headers: optional boolean array indicating whether
+ * the server should have seen a Cache-Control:max-age=0 header in the
+ * request.
+ */
+
+var now = new Date();
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+function make_url(uuid, id, value, content, info) {
+ var dates = {
+ fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(),
+ stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(),
+ };
+ var vary = "";
+ if ("vary" in info) {
+ vary = "&vary=" + info.vary;
+ }
+ var cache_control = "";
+ if ("cache_control" in info) {
+ cache_control = "&cache_control=" + info.cache_control;
+ }
+ var redirect = "";
+
+ var ignore_request_headers = "";
+ if ("request_headers" in info) {
+ // Ignore the request headers that we send since they may be synthesized by the test.
+ ignore_request_headers = "&ignore";
+ }
+ var url_sans_redirect = "resources/cache.py?token=" + uuid +
+ "&content=" + content +
+ "&" + id + "=" + value +
+ "&expires=" + dates[info.state] +
+ vary + cache_control + ignore_request_headers;
+ // If there's a redirect, the target is the script without any redirect at
+ // either the same domain or a different domain.
+ if ("redirect" in info) {
+ var host_info = get_host_info();
+ var origin;
+ switch (info.redirect) {
+ case "same-origin":
+ origin = host_info['HTTP_ORIGIN'];
+ break;
+ case "cross-origin":
+ origin = host_info['HTTP_REMOTE_ORIGIN'];
+ break;
+ }
+ var redirected_url = origin + base_path() + url_sans_redirect;
+ return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url);
+ } else {
+ return url_sans_redirect;
+ }
+}
+function expected_status(type, identifier, init) {
+ if (type == "date" &&
+ init.headers &&
+ init.headers["If-Modified-Since"] == identifier) {
+ // The server will respond with a 304 in this case.
+ return [304, "Not Modified"];
+ }
+ return [200, "OK"];
+}
+function expected_response_text(type, identifier, init, content) {
+ if (type == "date" &&
+ init.headers &&
+ init.headers["If-Modified-Since"] == identifier) {
+ // The server will respond with a 304 in this case.
+ return "";
+ }
+ return content;
+}
+function server_state(uuid) {
+ return fetch("resources/cache.py?querystate&token=" + uuid)
+ .then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ // null will be returned if the server never received any requests
+ // for the given uuid. Normalize that to an empty list consistent
+ // with our representation.
+ return JSON.parse(text) || [];
+ });
+}
+function make_test(type, info) {
+ return function(test) {
+ var uuid = token();
+ var identifier = (type == "tag" ? Math.random() : now.toGMTString());
+ var content = Math.random().toString();
+ var url = make_url(uuid, type, identifier, content, info);
+ var fetch_functions = [];
+ for (var i = 0; i < info.request_cache.length; ++i) {
+ fetch_functions.push(function(idx) {
+ var init = {cache: info.request_cache[idx]};
+ if ("request_headers" in info) {
+ init.headers = info.request_headers[idx];
+ }
+ if (init.cache === "only-if-cached") {
+ // only-if-cached requires we use same-origin mode.
+ init.mode = "same-origin";
+ }
+ return fetch(url, init)
+ .then(function(response) {
+ if ("response" in info && info.response[idx] === "error") {
+ assert_true(false, "fetch should have been an error");
+ return;
+ }
+ assert_array_equals([response.status, response.statusText],
+ expected_status(type, identifier, init));
+ return response.text();
+ }).then(function(text) {
+ assert_equals(text, expected_response_text(type, identifier, init, content));
+ }, function(reason) {
+ if ("response" in info && info.response[idx] === "error") {
+ assert_throws_js(TypeError, function() { throw reason; });
+ } else {
+ throw reason;
+ }
+ });
+ });
+ }
+ var i = 0;
+ function run_next_step() {
+ if (fetch_functions.length) {
+ return fetch_functions.shift()(i++)
+ .then(run_next_step);
+ } else {
+ return Promise.resolve();
+ }
+ }
+ return run_next_step()
+ .then(function() {
+ // Now, query the server state
+ return server_state(uuid);
+ }).then(function(state) {
+ var expectedState = [];
+ info.expected_validation_headers.forEach(function (validate) {
+ if (validate) {
+ if (type == "tag") {
+ expectedState.push({"If-None-Match": '"' + identifier + '"'});
+ } else {
+ expectedState.push({"If-Modified-Since": identifier});
+ }
+ } else {
+ expectedState.push({});
+ }
+ });
+ for (var i = 0; i < info.expected_no_cache_headers.length; ++i) {
+ if (info.expected_no_cache_headers[i]) {
+ expectedState[i]["Pragma"] = "no-cache";
+ expectedState[i]["Cache-Control"] = "no-cache";
+ }
+ }
+ if ("expected_max_age_headers" in info) {
+ for (var i = 0; i < info.expected_max_age_headers.length; ++i) {
+ if (info.expected_max_age_headers[i]) {
+ expectedState[i]["Cache-Control"] = "max-age=0";
+ }
+ }
+ }
+ assert_equals(state.length, expectedState.length);
+ for (var i = 0; i < state.length; ++i) {
+ for (var header in state[i]) {
+ assert_equals(state[i][header], expectedState[i][header]);
+ delete expectedState[i][header];
+ }
+ for (var header in expectedState[i]) {
+ assert_false(header in state[i]);
+ }
+ }
+ });
+ };
+}
+
+function run_tests(tests)
+{
+ tests.forEach(function(info) {
+ promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response");
+ promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response");
+ });
+}
diff --git a/test/fixtures/wpt/fetch/api/request/request-clone.sub.html b/test/fixtures/wpt/fetch/api/request/request-clone.sub.html
new file mode 100644
index 00000000000000..c690bb3dc03653
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-clone.sub.html
@@ -0,0 +1,63 @@
+
+
+
+
+ Request clone
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js b/test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js
new file mode 100644
index 00000000000000..c2bbf86f304c2f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js
@@ -0,0 +1,37 @@
+promise_test(async function () {
+ const req1 = new Request("https://example.com/", {
+ body: "req1",
+ method: "POST",
+ });
+
+ const text1 = await req1.text();
+ assert_equals(
+ text1,
+ "req1",
+ "The body of the first request should be 'req1'."
+ );
+
+ const req2 = new Request(req1, { body: "req2" });
+ const bodyText = await req2.text();
+ assert_equals(
+ bodyText,
+ "req2",
+ "The body of the second request should be overridden to 'req2'."
+ );
+
+}, "Check that the body of a new request can be overridden when created from an existing Request object");
+
+promise_test(async function () {
+ const req1 = new Request("https://example.com/", {
+ body: "req1",
+ method: "POST",
+ });
+
+ const req2 = new Request("https://example.com/", req1);
+ const bodyText = await req2.text();
+ assert_equals(
+ bodyText,
+ "req1",
+ "The body of the second request should be the same as the first."
+ );
+}, "Check that the body of a new request can be duplicated from an existing Request object");
diff --git a/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js b/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js
new file mode 100644
index 00000000000000..0bf9672a795057
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js
@@ -0,0 +1,89 @@
+// META: global=window,worker
+// META: title=Request consume empty bodies
+
+function checkBodyText(test, request) {
+ return request.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, "", "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+}
+
+async function checkBodyBlob(test, request) {
+ const bodyAsBlob = await request.blob();
+ const body = await bodyAsBlob.text();
+ assert_equals(body, "", "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+}
+
+function checkBodyArrayBuffer(test, request) {
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyJSON(test, request) {
+ return request.json().then(
+ function(bodyAsJSON) {
+ assert_unreached("JSON parsing should fail");
+ },
+ function() {
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyFormData(test, request) {
+ return request.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyFormDataError(test, request) {
+ return promise_rejects_js(test, TypeError, request.formData()).then(function() {
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkRequestWithNoBody(bodyType, checkFunction, headers = []) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "headers": headers});
+ assert_false(request.bodyUsed);
+ return checkFunction(test, request);
+ }, "Consume request's body as " + bodyType);
+}
+
+checkRequestWithNoBody("text", checkBodyText);
+checkRequestWithNoBody("blob", checkBodyBlob);
+checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer);
+checkRequestWithNoBody("json (error case)", checkBodyJSON);
+checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
+checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
+checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
+
+function checkRequestWithEmptyBody(bodyType, body, asText) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body});
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ if (asText) {
+ return request.text().then(function(bodyAsString) {
+ assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
+ assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer"));
+}
+
+// FIXME: Add BufferSource, FormData and URLSearchParams.
+checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
+checkRequestWithEmptyBody("text", "", false);
+checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
+checkRequestWithEmptyBody("text", "", true);
+checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
+// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950.
+checkRequestWithEmptyBody("FormData", new FormData(), true);
+checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);
diff --git a/test/fixtures/wpt/fetch/api/request/request-consume.any.js b/test/fixtures/wpt/fetch/api/request/request-consume.any.js
new file mode 100644
index 00000000000000..b4cbe7457d20a9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-consume.any.js
@@ -0,0 +1,148 @@
+// META: global=window,worker
+// META: title=Request consume
+// META: script=../resources/utils.js
+
+function checkBodyText(request, expectedBody) {
+ return request.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as text: bodyUsed turned true");
+ });
+}
+
+async function checkBodyBlob(request, expectedBody, checkContentType) {
+ const bodyAsBlob = await request.blob();
+
+ if (checkContentType)
+ assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type");
+
+ const body = await bodyAsBlob.text();
+ assert_equals(body, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as blob: bodyUsed turned true");
+}
+
+function checkBodyArrayBuffer(request, expectedBody) {
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
+ });
+}
+
+function checkBodyBytes(request, expectedBody) {
+ return request.bytes().then(function(bodyAsUint8Array) {
+ assert_true(bodyAsUint8Array instanceof Uint8Array);
+ validateBufferFromString(bodyAsUint8Array.buffer, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as bytes: bodyUsed turned true");
+ });
+}
+
+function checkBodyJSON(request, expectedBody) {
+ return request.json().then(function(bodyAsJSON) {
+ var strBody = JSON.stringify(bodyAsJSON)
+ assert_equals(strBody, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as json: bodyUsed turned true");
+ });
+}
+
+function checkBodyFormData(request, expectedBody) {
+ return request.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_true(request.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+}
+
+function checkRequestBody(body, expected, bodyType) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyText(request, expected);
+ }, "Consume " + bodyType + " request's body as text");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyBlob(request, expected);
+ }, "Consume " + bodyType + " request's body as blob");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyArrayBuffer(request, expected);
+ }, "Consume " + bodyType + " request's body as arrayBuffer");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyBytes(request, expected);
+ }, "Consume " + bodyType + " request's body as bytes");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyJSON(request, expected);
+ }, "Consume " + bodyType + " request's body as JSON");
+}
+
+var textData = JSON.stringify("This is response's body");
+var blob = new Blob([textData], { "type" : "text/plain" });
+
+checkRequestBody(textData, textData, "String");
+
+var string = "\"123456\"";
+function getArrayBuffer() {
+ var arrayBuffer = new ArrayBuffer(8);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < 8; cptr++)
+ int8Array[cptr] = string.charCodeAt(cptr);
+ return arrayBuffer;
+}
+
+function getArrayBufferWithZeros() {
+ var arrayBuffer = new ArrayBuffer(10);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < 8; cptr++)
+ int8Array[cptr + 1] = string.charCodeAt(cptr);
+ return arrayBuffer;
+}
+
+checkRequestBody(getArrayBuffer(), string, "ArrayBuffer");
+checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array");
+checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array");
+checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array");
+checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView");
+
+promise_test(function(test) {
+ var formData = new FormData();
+ formData.append("name", "value")
+ var request = new Request("", {"method": "POST", "body": formData });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyFormData(request, formData);
+}, "Consume FormData request's body as FormData");
+
+function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) {
+ promise_test(function(test) {
+ var response = new Response(blobBody);
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ return checkFunction(response, blobData);
+ }, "Consume blob response's body as " + bodyType);
+}
+
+checkBlobResponseBody(blob, textData, "blob", checkBodyBlob);
+checkBlobResponseBody(blob, textData, "text", checkBodyText);
+checkBlobResponseBody(blob, textData, "json", checkBodyJSON);
+checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer);
+checkBlobResponseBody(blob, textData, "bytes", checkBodyBytes);
+checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob);
+
+var goodJSONValues = ["null", "1", "true", "\"string\""];
+goodJSONValues.forEach(function(value) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": value});
+ return request.json().then(function(v) {
+ assert_equals(v, JSON.parse(value));
+ });
+ }, "Consume JSON from text: '" + JSON.stringify(value) + "'");
+});
+
+var badJSONValues = ["undefined", "{", "a", "["];
+badJSONValues.forEach(function(value) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": value});
+ return promise_rejects_js(test, SyntaxError, request.json());
+ }, "Trying to consume bad JSON text as JSON: '" + value + "'");
+});
diff --git a/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js b/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js
new file mode 100644
index 00000000000000..8a11de78ff6e0e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js
@@ -0,0 +1,109 @@
+// META: global=window,worker
+// META: title=Request disturbed
+// META: script=../resources/utils.js
+
+var initValuesDict = {"method" : "POST",
+ "body" : "Request's body"
+};
+
+var noBodyConsumed = new Request("");
+var bodyConsumed = new Request("", initValuesDict);
+
+test(() => {
+ assert_equals(noBodyConsumed.body, null, "body's default value is null");
+ assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed");
+ assert_not_equals(bodyConsumed.body, null, "non-null body");
+ assert_true(bodyConsumed.body instanceof ReadableStream, "non-null body type");
+ assert_false(noBodyConsumed.bodyUsed, "bodyUsed is false when request is not disturbed");
+}, "Request's body: initial state");
+
+noBodyConsumed.blob();
+bodyConsumed.blob();
+
+test(function() {
+ assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed");
+ try {
+ noBodyConsumed.clone();
+ } catch (e) {
+ assert_unreached("Can use request not disturbed for creating or cloning request");
+ }
+}, "Request without body cannot be disturbed");
+
+test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_throws_js(TypeError, function() { bodyConsumed.clone(); });
+}, "Check cloning a disturbed request");
+
+test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_throws_js(TypeError, function() { new Request(bodyConsumed); });
+}, "Check creating a new request from a disturbed request");
+
+promise_test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ const originalBody = bodyConsumed.body;
+ const bodyReplaced = new Request(bodyConsumed, { body: "Replaced body" });
+ assert_not_equals(bodyReplaced.body, originalBody, "new request's body is new");
+ assert_false(bodyReplaced.bodyUsed, "bodyUsed is false when request is not disturbed");
+ return bodyReplaced.text().then(text => {
+ assert_equals(text, "Replaced body");
+ });
+}, "Check creating a new request with a new body from a disturbed request");
+
+promise_test(function() {
+ var bodyRequest = new Request("", initValuesDict);
+ const originalBody = bodyRequest.body;
+ assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed");
+ var requestFromRequest = new Request(bodyRequest);
+ assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_equals(bodyRequest.body, originalBody, "body should not change");
+ assert_not_equals(originalBody, undefined, "body should not be undefined");
+ assert_not_equals(originalBody, null, "body should not be null");
+ assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new");
+ return requestFromRequest.text().then(text => {
+ assert_equals(text, "Request's body");
+ });
+}, "Input request used for creating new request became disturbed");
+
+promise_test(() => {
+ const bodyRequest = new Request("", initValuesDict);
+ const originalBody = bodyRequest.body;
+ assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed");
+ const requestFromRequest = new Request(bodyRequest, { body : "init body" });
+ assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_equals(bodyRequest.body, originalBody, "body should not change");
+ assert_not_equals(originalBody, undefined, "body should not be undefined");
+ assert_not_equals(originalBody, null, "body should not be null");
+ assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new");
+
+ return requestFromRequest.text().then(text => {
+ assert_equals(text, "init body");
+ });
+}, "Input request used for creating new request became disturbed even if body is not used");
+
+promise_test(function(test) {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ return promise_rejects_js(test, TypeError, bodyConsumed.blob());
+}, "Check consuming a disturbed request");
+
+test(function() {
+ var req = new Request(URL, {method: 'POST', body: 'hello'});
+ assert_false(req.bodyUsed,
+ 'Request should not be flagged as used if it has not been ' +
+ 'consumed.');
+ assert_throws_js(TypeError,
+ function() { new Request(req, {method: 'GET'}); },
+ 'A get request may not have body.');
+
+ assert_false(req.bodyUsed, 'After the GET case');
+
+ assert_throws_js(TypeError,
+ function() { new Request(req, {method: 'CONNECT'}); },
+ 'Request() with a forbidden method must throw.');
+
+ assert_false(req.bodyUsed, 'After the forbidden method case');
+
+ var req2 = new Request(req);
+ assert_true(req.bodyUsed,
+ 'Request should be flagged as used if it has been consumed.');
+}, 'Request construction failure should not set "bodyUsed"');
diff --git a/test/fixtures/wpt/fetch/api/request/request-error.any.js b/test/fixtures/wpt/fetch/api/request/request-error.any.js
new file mode 100644
index 00000000000000..9ec8015198dadd
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-error.any.js
@@ -0,0 +1,56 @@
+// META: global=window,worker
+// META: title=Request error
+// META: script=request-error.js
+
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ test(() => {
+ assert_throws_js(
+ TypeError,
+ () => new Request(...args),
+ "Expect TypeError exception"
+ );
+ }, testName);
+}
+
+test(function() {
+ assert_throws_js(
+ TypeError,
+ () => Request("about:blank"),
+ "Calling Request constructor without 'new' must throw"
+ );
+});
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from the init request");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var headers = new Headers([]);
+ var request = new Request(initialRequest, {"headers" : headers});
+ assert_false(request.headers.has("Content-Type"));
+}, "Request should not get its content-type from the init request if init headers are provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8");
+}, "Request should get its content-type from the body if none is provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from init headers if one is provided");
+
+test(function() {
+ var options = {"cache": "only-if-cached", "mode": "same-origin"};
+ new Request("test", options);
+}, "Request with cache mode: only-if-cached and fetch mode: same-origin");
diff --git a/test/fixtures/wpt/fetch/api/request/request-error.js b/test/fixtures/wpt/fetch/api/request/request-error.js
new file mode 100644
index 00000000000000..cf77313f5bc309
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-error.js
@@ -0,0 +1,57 @@
+const badRequestArgTests = [
+ {
+ args: ["", { "window": "http://test.url" }],
+ testName: "RequestInit's window is not null"
+ },
+ {
+ args: ["http://:not a valid URL"],
+ testName: "Input URL is not valid"
+ },
+ {
+ args: ["http://user:pass@test.url"],
+ testName: "Input URL has credentials"
+ },
+ {
+ args: ["", { "mode": "navigate" }],
+ testName: "RequestInit's mode is navigate"
+ },
+ {
+ args: ["", { "referrer": "http://:not a valid URL" }],
+ testName: "RequestInit's referrer is invalid"
+ },
+ {
+ args: ["", { "method": "IN VALID" }],
+ testName: "RequestInit's method is invalid"
+ },
+ {
+ args: ["", { "method": "TRACE" }],
+ testName: "RequestInit's method is forbidden"
+ },
+ {
+ args: ["", { "mode": "no-cors", "method": "PUT" }],
+ testName: "RequestInit's mode is no-cors and method is not simple"
+ },
+ {
+ args: ["", { "mode": "cors", "cache": "only-if-cached" }],
+ testName: "RequestInit's cache mode is only-if-cached and mode is not same-origin"
+ },
+ {
+ args: ["test", { "cache": "only-if-cached", "mode": "cors" }],
+ testName: "Request with cache mode: only-if-cached and fetch mode cors"
+ },
+ {
+ args: ["test", { "cache": "only-if-cached", "mode": "no-cors" }],
+ testName: "Request with cache mode: only-if-cached and fetch mode no-cors"
+ }
+];
+
+badRequestArgTests.push(
+ ...["referrerPolicy", "mode", "credentials", "cache", "redirect"].map(optionProp => {
+ const options = {};
+ options[optionProp] = "BAD";
+ return {
+ args: ["", options],
+ testName: `Bad ${optionProp} init parameter value`
+ };
+ })
+);
diff --git a/test/fixtures/wpt/fetch/api/request/request-headers.any.js b/test/fixtures/wpt/fetch/api/request/request-headers.any.js
new file mode 100644
index 00000000000000..a766bcb5fff6b3
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-headers.any.js
@@ -0,0 +1,177 @@
+// META: global=window,worker
+// META: title=Request Headers
+
+var validRequestHeaders = [
+ ["Content-Type", "OK"],
+ ["Potato", "OK"],
+ ["proxy", "OK"],
+ ["proxya", "OK"],
+ ["sec", "OK"],
+ ["secb", "OK"],
+ ["Set-Cookie2", "OK"],
+ ["User-Agent", "OK"],
+];
+var invalidRequestHeaders = [
+ ["Accept-Charset", "KO"],
+ ["accept-charset", "KO"],
+ ["ACCEPT-ENCODING", "KO"],
+ ["Accept-Encoding", "KO"],
+ ["Access-Control-Request-Headers", "KO"],
+ ["Access-Control-Request-Method", "KO"],
+ ["Connection", "KO"],
+ ["Content-Length", "KO"],
+ ["Cookie", "KO"],
+ ["Cookie2", "KO"],
+ ["Date", "KO"],
+ ["DNT", "KO"],
+ ["Expect", "KO"],
+ ["Host", "KO"],
+ ["Keep-Alive", "KO"],
+ ["Origin", "KO"],
+ ["Referer", "KO"],
+ ["Set-Cookie", "KO"],
+ ["TE", "KO"],
+ ["Trailer", "KO"],
+ ["Transfer-Encoding", "KO"],
+ ["Upgrade", "KO"],
+ ["Via", "KO"],
+ ["Proxy-", "KO"],
+ ["proxy-a", "KO"],
+ ["Sec-", "KO"],
+ ["sec-b", "KO"],
+];
+
+var validRequestNoCorsHeaders = [
+ ["Accept", "OK"],
+ ["Accept-Language", "OK"],
+ ["content-language", "OK"],
+ ["content-type", "application/x-www-form-urlencoded"],
+ ["content-type", "application/x-www-form-urlencoded;charset=UTF-8"],
+ ["content-type", "multipart/form-data"],
+ ["content-type", "multipart/form-data;charset=UTF-8"],
+ ["content-TYPE", "text/plain"],
+ ["CONTENT-type", "text/plain;charset=UTF-8"],
+];
+var invalidRequestNoCorsHeaders = [
+ ["Content-Type", "KO"],
+ ["Potato", "KO"],
+ ["proxy", "KO"],
+ ["proxya", "KO"],
+ ["sec", "KO"],
+ ["secb", "KO"],
+ ["Empty-Value", ""],
+];
+
+validRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), header[1]);
+ }, "Adding valid request header \"" + header[0] + ": " + header[1] + "\"");
+});
+invalidRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), null);
+ }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\"");
+});
+
+validRequestNoCorsHeaders.forEach(function(header) {
+ test(function() {
+ var requestNoCors = new Request("", {"mode": "no-cors"});
+ requestNoCors.headers.set(header[0], header[1]);
+ assert_equals(requestNoCors.headers.get(header[0]), header[1]);
+ }, "Adding valid no-cors request header \"" + header[0] + ": " + header[1] + "\"");
+});
+invalidRequestNoCorsHeaders.forEach(function(header) {
+ test(function() {
+ var requestNoCors = new Request("", {"mode": "no-cors"});
+ requestNoCors.headers.set(header[0], header[1]);
+ assert_equals(requestNoCors.headers.get(header[0]), null);
+ }, "Adding invalid no-cors request header \"" + header[0] + ": " + header[1] + "\"");
+});
+
+test(function() {
+ var headers = new Headers([["Cookie2", "potato"]]);
+ var request = new Request("", {"headers": headers});
+ assert_equals(request.headers.get("Cookie2"), null);
+}, "Check that request constructor is filtering headers provided as init parameter");
+
+test(function() {
+ var headers = new Headers([["Content-Type", "potato"]]);
+ var request = new Request("", {"headers": headers, "mode": "no-cors"});
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Check that no-cors request constructor is filtering headers provided as init parameter");
+
+test(function() {
+ var headers = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers": headers});
+ var request = new Request(initialRequest, {"mode": "no-cors"});
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Check that no-cors request constructor is filtering headers provided as part of request parameter");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from the init request");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var headers = new Headers([]);
+ var request = new Request(initialRequest, {"headers" : headers});
+ assert_false(request.headers.has("Content-Type"));
+}, "Request should not get its content-type from the init request if init headers are provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8");
+}, "Request should get its content-type from the body if none is provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from init headers if one is provided");
+
+test(function() {
+ var array = [["hello", "worldAHH"]];
+ var object = {"hello": 'worldOOH'};
+ var headers = new Headers(array);
+
+ assert_equals(headers.get("hello"), "worldAHH");
+
+ var request1 = new Request("", {"headers": headers});
+ var request2 = new Request("", {"headers": array});
+ var request3 = new Request("", {"headers": object});
+
+ assert_equals(request1.headers.get("hello"), "worldAHH");
+ assert_equals(request2.headers.get("hello"), "worldAHH");
+ assert_equals(request3.headers.get("hello"), "worldOOH");
+}, "Testing request header creations with various objects");
+
+promise_test(function(test) {
+ var request = new Request("", {"headers" : [["Content-Type", ""]], "body" : "this is my plate", "method" : "POST"});
+ return request.blob().then(function(blob) {
+ assert_equals(blob.type, "", "Blob type should be the empty string");
+ });
+}, "Testing empty Request Content-Type header");
+
+test(function() {
+ const request1 = new Request("");
+ assert_equals(request1.headers, request1.headers);
+
+ const request2 = new Request("", {"headers": {"X-Foo": "bar"}});
+ assert_equals(request2.headers, request2.headers);
+ const headers = request2.headers;
+ request2.headers.set("X-Foo", "quux");
+ assert_equals(headers, request2.headers);
+ headers.set("X-Other-Header", "baz");
+ assert_equals(headers, request2.headers);
+}, "Test that Request.headers has the [SameObject] extended attribute");
diff --git a/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html b/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html
new file mode 100644
index 00000000000000..cc495a66527d7b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html
@@ -0,0 +1,112 @@
+
+
+
+
+ Request init: simple cases
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-init-002.any.js b/test/fixtures/wpt/fetch/api/request/request-init-002.any.js
new file mode 100644
index 00000000000000..abb6689f1e844a
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-init-002.any.js
@@ -0,0 +1,60 @@
+// META: global=window,worker
+// META: title=Request init: headers and body
+
+test(function() {
+ var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3"
+ };
+ var headers = new Headers(headerDict);
+ var request = new Request("", { "headers" : headers })
+ for (var name in headerDict) {
+ assert_equals(request.headers.get(name), headerDict[name],
+ "request's headers has " + name + " : " + headerDict[name]);
+ }
+}, "Initialize Request with headers values");
+
+function makeRequestInit(body, method) {
+ return {"method": method, "body": body};
+}
+
+function checkRequestInit(body, bodyType, expectedTextBody) {
+ promise_test(function(test) {
+ var request = new Request("", makeRequestInit(body, "POST"));
+ if (body) {
+ assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "GET")); });
+ assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "HEAD")); });
+ } else {
+ new Request("", makeRequestInit(body, "GET")); // should not throw
+ }
+ var reqHeaders = request.headers;
+ var mime = reqHeaders.get("Content-Type");
+ assert_true(!body || (mime && mime.search(bodyType) > -1), "Content-Type header should be \"" + bodyType + "\", not \"" + mime + "\"");
+ return request.text().then(function(bodyAsText) {
+ //not equals: cannot guess formData exact value
+ assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body");
+ });
+ }, `Initialize Request's body with "${body}", ${bodyType}`);
+}
+
+var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
+var formaData = new FormData();
+formaData.append("name", "value");
+var usvString = "This is a USVString"
+
+checkRequestInit(undefined, undefined, "");
+checkRequestInit(null, null, "");
+checkRequestInit(blob, "application/octet-binary", "This is a blob");
+checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
+checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!");
+
+// Ensure test does not time out in case of missing URLSearchParams support.
+if (self.URLSearchParams) {
+ var urlSearchParams = new URLSearchParams("name=value");
+ checkRequestInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value");
+} else {
+ promise_test(function(test) {
+ return Promise.reject("URLSearchParams not supported");
+ }, "Initialize Request's body with application/x-www-form-urlencoded;charset=UTF-8");
+}
diff --git a/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html b/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html
new file mode 100644
index 00000000000000..79c91cdfe82af4
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html
@@ -0,0 +1,84 @@
+
+
+
+
+ Request: init with request or url
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js b/test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js
new file mode 100644
index 00000000000000..18a6969d4f8acd
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js
@@ -0,0 +1,141 @@
+function requestFromBody(body) {
+ return new Request(
+ "https://example.com",
+ {
+ method: "POST",
+ body,
+ duplex: "half",
+ },
+ );
+}
+
+test(() => {
+ const request = requestFromBody(undefined);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), "a/b; c=d");
+}, "Default Content-Type for Request with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const request = requestFromBody(buffer);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with buffer source body");
+
+promise_test(async () => {
+ const formData = new FormData();
+ formData.append("a", "b");
+ const request = requestFromBody(formData);
+ const boundary = (await request.text()).split("\r\n")[0].slice(2);
+ assert_equals(
+ request.headers.get("Content-Type"),
+ `multipart/form-data; boundary=${boundary}`,
+ );
+}, "Default Content-Type for Request with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const request = requestFromBody(usp);
+ assert_equals(
+ request.headers.get("Content-Type"),
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ );
+}, "Default Content-Type for Request with URLSearchParams body");
+
+test(() => {
+ const request = requestFromBody("");
+ assert_equals(
+ request.headers.get("Content-Type"),
+ "text/plain;charset=UTF-8",
+ );
+}, "Default Content-Type for Request with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const request = requestFromBody(stream);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with ReadableStream body");
+
+// -----------------------------------------------------------------------------
+
+const OVERRIDE_MIME = "test/only; mime=type";
+
+function requestFromBodyWithOverrideMime(body) {
+ return new Request(
+ "https://example.com",
+ {
+ method: "POST",
+ body,
+ headers: { "Content-Type": OVERRIDE_MIME },
+ duplex: "half",
+ },
+ );
+}
+
+test(() => {
+ const request = requestFromBodyWithOverrideMime(undefined);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const request = requestFromBodyWithOverrideMime(buffer);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with buffer source body");
+
+test(() => {
+ const formData = new FormData();
+ const request = requestFromBodyWithOverrideMime(formData);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const request = requestFromBodyWithOverrideMime(usp);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with URLSearchParams body");
+
+test(() => {
+ const request = requestFromBodyWithOverrideMime("");
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const request = requestFromBodyWithOverrideMime(stream);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with ReadableStream body");
diff --git a/test/fixtures/wpt/fetch/api/request/request-init-priority.any.js b/test/fixtures/wpt/fetch/api/request/request-init-priority.any.js
new file mode 100644
index 00000000000000..eb5073c85785c3
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-init-priority.any.js
@@ -0,0 +1,26 @@
+var priorities = ["high",
+ "low",
+ "auto"
+ ];
+
+for (idx in priorities) {
+ test(() => {
+ new Request("", {priority: priorities[idx]});
+ }, "new Request() with a '" + priorities[idx] + "' priority does not throw an error");
+}
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new Request("", {priority: 'invalid'});
+ }, "a new Request() must throw a TypeError if RequestInit's priority is an invalid value");
+}, "new Request() throws a TypeError if any of RequestInit's members' values are invalid");
+
+for (idx in priorities) {
+ promise_test(function(t) {
+ return fetch('hello.txt', { priority: priorities[idx] });
+ }, "fetch() with a '" + priorities[idx] + "' priority completes successfully");
+}
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError, fetch('hello.txt', { priority: 'invalid' }));
+}, "fetch() with an invalid priority returns a rejected promise with a TypeError");
diff --git a/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js b/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js
new file mode 100644
index 00000000000000..f0ae441a00258d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js
@@ -0,0 +1,147 @@
+// META: global=window,worker
+
+"use strict";
+
+const duplex = "half";
+const method = "POST";
+
+test(() => {
+ const body = new ReadableStream();
+ const request = new Request("...", { method, body, duplex });
+ assert_equals(request.body, body);
+}, "Constructing a Request with a stream holds the original object.");
+
+test((t) => {
+ const body = new ReadableStream();
+ body.getReader();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which getReader() is called");
+
+test((t) => {
+ const body = new ReadableStream();
+ body.getReader().read();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which read() is called");
+
+promise_test(async (t) => {
+ const body = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) });
+ const reader = body.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which read() and releaseLock() are called");
+
+test((t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ request.body.getReader();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which body.getReader() is called");
+
+test((t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ request.body.getReader().read();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which body.getReader().read() is called");
+
+promise_test(async (t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ const reader = request.body.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which read() and releaseLock() are called");
+
+test((t) => {
+ new Request("...", { method, body: null });
+}, "It is OK to omit .duplex when the body is null.");
+
+test((t) => {
+ new Request("...", { method, body: "..." });
+}, "It is OK to omit .duplex when the body is a string.");
+
+test((t) => {
+ new Request("...", { method, body: new Uint8Array(3) });
+}, "It is OK to omit .duplex when the body is a Uint8Array.");
+
+test((t) => {
+ new Request("...", { method, body: new Blob([]) });
+}, "It is OK to omit .duplex when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body }));
+}, "It is error to omit .duplex when the body is a ReadableStream.");
+
+test((t) => {
+ new Request("...", { method, body: null, duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is null.");
+
+test((t) => {
+ new Request("...", { method, body: "...", duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a string.");
+
+test((t) => {
+ new Request("...", { method, body: new Uint8Array(3), duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a Uint8Array.");
+
+test((t) => {
+ new Request("...", { method, body: new Blob([]), duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ new Request("...", { method, body, duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a ReadableStream.");
+
+test((t) => {
+ const body = null;
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is null.");
+
+test((t) => {
+ const body = "...";
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a string.");
+
+test((t) => {
+ const body = new Uint8Array(3);
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a Uint8Array.");
+
+test((t) => {
+ const body = new Blob([]);
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a ReadableStream.");
+
+test((t) => {
+ const body = new ReadableStream();
+ const duplex = "half";
+ const req1 = new Request("...", { method, body, duplex });
+ const req2 = new Request(req1);
+}, "It is OK to omit duplex when init.body is not given and input.body is given.");
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html b/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html
new file mode 100644
index 00000000000000..548ab38d7e14d8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html
@@ -0,0 +1,97 @@
+
+
+
+
+ Request Keepalive Quota Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js b/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js
new file mode 100644
index 00000000000000..cb4506db46c931
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+// META: title=Request keepalive
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+test(() => {
+ assert_false(new Request('/').keepalive, 'default');
+ assert_true(new Request('/', {keepalive: true}).keepalive, 'true');
+ assert_false(new Request('/', {keepalive: false}).keepalive, 'false');
+ assert_true(new Request('/', {keepalive: 1}).keepalive, 'truish');
+ assert_false(new Request('/', {keepalive: 0}).keepalive, 'falsy');
+}, 'keepalive flag');
+
+test(() => {
+ const init = {method: 'POST', keepalive: true, body: new ReadableStream()};
+ assert_throws_js(TypeError, () => {new Request('/', init)});
+}, 'keepalive flag with stream body');
diff --git a/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html b/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html
new file mode 100644
index 00000000000000..7be3608d737c34
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/request/request-structure.any.js b/test/fixtures/wpt/fetch/api/request/request-structure.any.js
new file mode 100644
index 00000000000000..5e7855385554bd
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/request-structure.any.js
@@ -0,0 +1,143 @@
+// META: global=window,worker
+// META: title=Request structure
+
+var request = new Request("");
+var methods = ["clone",
+ //Request implements Body
+ "arrayBuffer",
+ "blob",
+ "formData",
+ "json",
+ "text"
+ ];
+var attributes = ["method",
+ "url",
+ "headers",
+ "destination",
+ "referrer",
+ "referrerPolicy",
+ "mode",
+ "credentials",
+ "cache",
+ "redirect",
+ "integrity",
+ "isReloadNavigation",
+ "isHistoryNavigation",
+ "duplex",
+ //Request implements Body
+ "bodyUsed"
+ ];
+var internalAttributes = ["priority",
+ "internalpriority",
+ "blocking"
+ ];
+
+function isReadOnly(request, attributeToCheck) {
+ var defaultValue = undefined;
+ var newValue = undefined;
+ switch (attributeToCheck) {
+ case "method":
+ defaultValue = "GET";
+ newValue = "POST";
+ break;
+
+ case "url":
+ //default value is base url
+ //i.e http://example.com/fetch/api/request-structure.html
+ newValue = "http://url.test";
+ break;
+
+ case "headers":
+ request.headers = new Headers ({"name":"value"});
+ assert_false(request.headers.has("name"), "Headers attribute is read only");
+ return;
+
+ case "destination":
+ defaultValue = "";
+ newValue = "worker";
+ break;
+
+ case "referrer":
+ defaultValue = "about:client";
+ newValue = "http://url.test";
+ break;
+
+ case "referrerPolicy":
+ defaultValue = "";
+ newValue = "unsafe-url";
+ break;
+
+ case "mode":
+ defaultValue = "cors";
+ newValue = "navigate";
+ break;
+
+ case "credentials":
+ defaultValue = "same-origin";
+ newValue = "cors";
+ break;
+
+ case "cache":
+ defaultValue = "default";
+ newValue = "reload";
+ break;
+
+ case "redirect":
+ defaultValue = "follow";
+ newValue = "manual";
+ break;
+
+ case "integrity":
+ newValue = "CannotWriteIntegrity";
+ break;
+
+ case "bodyUsed":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "isReloadNavigation":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "isHistoryNavigation":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "duplex":
+ defaultValue = "half";
+ newValue = "full";
+ break;
+
+ default:
+ return;
+ }
+
+ request[attributeToCheck] = newValue;
+ if (defaultValue === undefined)
+ assert_not_equals(request[attributeToCheck], newValue, "Attribute " + attributeToCheck + " is read only");
+ else
+ assert_equals(request[attributeToCheck], defaultValue,
+ "Attribute " + attributeToCheck + " is read only. Default value is " + defaultValue);
+}
+
+for (var idx in methods) {
+ test(function() {
+ assert_true(methods[idx] in request, "request has " + methods[idx] + " method");
+ }, "Request has " + methods[idx] + " method");
+}
+
+for (var idx in attributes) {
+ test(function() {
+ assert_true(attributes[idx] in request, "request has " + attributes[idx] + " attribute");
+ isReadOnly(request, attributes[idx]);
+ }, "Check " + attributes[idx] + " attribute");
+}
+
+for (var idx in internalAttributes) {
+ test(function() {
+ assert_false(internalAttributes[idx] in request, "request does not expose " + internalAttributes[idx] + " attribute");
+ }, "Request does not expose " + internalAttributes[idx] + " attribute");
+}
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/request/resources/hello.txt b/test/fixtures/wpt/fetch/api/request/resources/hello.txt
new file mode 100644
index 00000000000000..ce013625030ba8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/resources/hello.txt
@@ -0,0 +1 @@
+hello
diff --git a/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js b/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js
new file mode 100644
index 00000000000000..4b264ca2fec3ba
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ const name = params.get('name');
+ const old_attribute = event.request[name];
+ // If any of |init|'s member is present...
+ const init = {cache: 'no-store'}
+ const new_attribute = (new Request(event.request, init))[name];
+
+ event.respondWith(
+ new Response(`old: ${old_attribute}, new: ${new_attribute}`));
+ });
diff --git a/test/fixtures/wpt/fetch/api/request/url-encoding.html b/test/fixtures/wpt/fetch/api/request/url-encoding.html
new file mode 100644
index 00000000000000..31c1ed3920bf9f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/request/url-encoding.html
@@ -0,0 +1,25 @@
+
+
+Fetch: URL encoding
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/resources/basic.html b/test/fixtures/wpt/fetch/api/resources/basic.html
new file mode 100644
index 00000000000000..e23afd4bf6a7ec
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/basic.html
@@ -0,0 +1,5 @@
+
+
diff --git a/test/fixtures/wpt/fetch/api/resources/cors-top.txt b/test/fixtures/wpt/fetch/api/resources/cors-top.txt
new file mode 100644
index 00000000000000..83a3157d14d908
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/cors-top.txt
@@ -0,0 +1 @@
+top
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers b/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers
new file mode 100644
index 00000000000000..cb762eff806849
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/test/fixtures/wpt/fetch/api/resources/data.json b/test/fixtures/wpt/fetch/api/resources/data.json
new file mode 100644
index 00000000000000..76519fa8cc27ab
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/data.json
@@ -0,0 +1 @@
+{"key": "value"}
diff --git a/test/fixtures/wpt/fetch/api/resources/empty.txt b/test/fixtures/wpt/fetch/api/resources/empty.txt
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-helper.js b/test/fixtures/wpt/fetch/api/resources/keepalive-helper.js
new file mode 100644
index 00000000000000..ad0e9bfa06c0bb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/keepalive-helper.js
@@ -0,0 +1,199 @@
+// Utility functions to help testing keepalive requests.
+
+// Returns a URL to an iframe that loads a keepalive URL on iframe loaded.
+//
+// The keepalive URL points to a target that stores `token`. The token will then
+// be posted back on iframe loaded to the parent document.
+// `method` defaults to GET.
+// `frameOrigin` to specify the origin of the iframe to load. If not set,
+// default to a different site origin.
+// `requestOrigin` to specify the origin of the fetch request target.
+// `sendOn` to specify the name of the event when the keepalive request should
+// be sent instead of the default 'load'.
+// `mode` to specify the fetch request's CORS mode.
+// `disallowCrossOrigin` to ask the iframe to set up a server that disallows
+// cross origin requests.
+function getKeepAliveIframeUrl(token, method, {
+ frameOrigin = 'DEFAULT',
+ requestOrigin = '',
+ sendOn = 'load',
+ mode = 'cors',
+ disallowCrossOrigin = false
+} = {}) {
+ const https = location.protocol.startsWith('https');
+ frameOrigin = frameOrigin === 'DEFAULT' ?
+ get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'] :
+ frameOrigin;
+ return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` +
+ `token=${token}&` +
+ `method=${method}&` +
+ `sendOn=${sendOn}&` +
+ `mode=${mode}&` + (disallowCrossOrigin ? `disallowCrossOrigin=1&` : ``) +
+ `origin=${requestOrigin}`;
+}
+
+// Returns a different-site URL to an iframe that loads a keepalive URL.
+//
+// By default, the keepalive URL points to a target that redirects to another
+// same-origin destination storing `token`. The token will then be posted back
+// to parent document.
+//
+// The URL redirects can be customized from `origin1` to `origin2` if provided.
+// Sets `withPreflight` to true to get URL enabling preflight.
+function getKeepAliveAndRedirectIframeUrl(
+ token, origin1, origin2, withPreflight) {
+ const https = location.protocol.startsWith('https');
+ const frameOrigin =
+ get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'];
+ return `${frameOrigin}/fetch/api/resources/keepalive-redirect-iframe.html?` +
+ `token=${token}&` +
+ `origin1=${origin1}&` +
+ `origin2=${origin2}&` + (withPreflight ? `with-headers` : ``);
+}
+
+async function iframeLoaded(iframe) {
+ return new Promise((resolve) => iframe.addEventListener('load', resolve));
+}
+
+// Obtains the token from the message posted by iframe after loading
+// `getKeepAliveAndRedirectIframeUrl()`.
+async function getTokenFromMessage() {
+ return new Promise((resolve) => {
+ window.addEventListener('message', (event) => {
+ resolve(event.data);
+ }, {once: true});
+ });
+}
+
+// Tells if `token` has been stored in the server.
+async function queryToken(token) {
+ const response = await fetch(`../resources/stash-take.py?key=${token}`);
+ const json = await response.json();
+ return json;
+}
+
+// A helper to assert the existence of `token` that should have been stored in
+// the server by fetching ../resources/stash-put.py.
+//
+// This function simply wait for a custom amount of time before trying to
+// retrieve `token` from the server.
+// `expectTokenExist` tells if `token` should be present or not.
+//
+// NOTE:
+// In order to parallelize the work, we are going to have an async_test
+// for the rest of the work. Note that we want the serialized behavior
+// for the steps so far, so we don't want to make the entire test case
+// an async_test.
+function assertStashedTokenAsync(
+ testName, token, {expectTokenExist = true} = {}) {
+ async_test(test => {
+ new Promise(resolve => test.step_timeout(resolve, 3000 /*ms*/))
+ .then(test.step_func(() => {
+ return queryToken(token);
+ }))
+ .then(test.step_func(result => {
+ if (expectTokenExist) {
+ assert_equals(result, 'on', `token should be on (stashed).`);
+ test.done();
+ } else {
+ assert_not_equals(
+ result, 'on', `token should not be on (stashed).`);
+ return Promise.reject(`Failed to retrieve token from server`);
+ }
+ }))
+ .catch(test.step_func(e => {
+ if (expectTokenExist) {
+ test.unreached_func(e);
+ } else {
+ test.done();
+ }
+ }));
+ }, testName);
+}
+
+/**
+ * In an iframe, and in `load` event handler, test to fetch a keepalive URL that
+ * involves in redirect to another URL.
+ *
+ * `unloadIframe` to unload the iframe before verifying stashed token to
+ * simulate the situation that unloads after fetching. Note that this test is
+ * different from `keepaliveRedirectInUnloadTest()` in that the latter
+ * performs fetch() call directly in `unload` event handler, while this test
+ * does it in `load`.
+ */
+function keepaliveRedirectTest(desc, {
+ origin1 = '',
+ origin2 = '',
+ withPreflight = false,
+ unloadIframe = false,
+ expectFetchSucceed = true,
+} = {}) {
+ desc = `[keepalive][iframe][load] ${desc}` +
+ (unloadIframe ? ' [unload at end]' : '');
+ promise_test(async (test) => {
+ const tokenToStash = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveAndRedirectIframeUrl(
+ tokenToStash, origin1, origin2, withPreflight);
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ assert_equals(await getTokenFromMessage(), tokenToStash);
+ if (unloadIframe) {
+ iframe.remove();
+ }
+
+ assertStashedTokenAsync(
+ desc, tokenToStash, {expectTokenExist: expectFetchSucceed});
+ }, `${desc}; setting up`);
+}
+
+/**
+ * Opens a different site window, and in `unload` event handler, test to fetch
+ * a keepalive URL that involves in redirect to another URL.
+ */
+function keepaliveRedirectInUnloadTest(desc, {
+ origin1 = '',
+ origin2 = '',
+ url2 = '',
+ withPreflight = false,
+ expectFetchSucceed = true
+} = {}) {
+ desc = `[keepalive][new window][unload] ${desc}`;
+
+ promise_test(async (test) => {
+ const targetUrl =
+ `${HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html?` +
+ `origin1=${origin1}&` +
+ `origin2=${origin2}&` +
+ `url2=${url2}&` + (withPreflight ? `with-headers` : ``);
+ const w = window.open(targetUrl);
+ const token = await getTokenFromMessage();
+ w.close();
+
+ assertStashedTokenAsync(
+ desc, token, {expectTokenExist: expectFetchSucceed});
+ }, `${desc}; setting up`);
+}
+
+/**
+* utility to create pending keepalive fetch requests
+* The pending request state is achieved by ensuring the server (trickle.py) does not
+* immediately respond to the fetch requests.
+* The response delay is set as a url parameter.
+*/
+
+function createPendingKeepAliveRequest(delay, remote = false) {
+ // trickle.py is a script that can make a delayed response to the client request
+ const trickleRemoteURL = get_host_info().HTTPS_REMOTE_ORIGIN + '/fetch/api/resources/trickle.py?count=1&ms=';
+ const trickleLocalURL = get_host_info().HTTP_ORIGIN + '/fetch/api/resources/trickle.py?count=1&ms=';
+ url = remote ? trickleRemoteURL : trickleLocalURL;
+
+ const body = '*'.repeat(10);
+ return fetch(url + delay, { keepalive: true, body, method: 'POST' }).then(res => {
+ return res.text();
+ }).then(() => {
+ return new Promise(resolve => step_timeout(resolve, 1));
+ }).catch((error) => {
+ return Promise.reject(error);;
+ })
+}
diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html b/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html
new file mode 100644
index 00000000000000..f9dae5a34ecdbd
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html b/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html
new file mode 100644
index 00000000000000..fdee00f3124792
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html b/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html
new file mode 100644
index 00000000000000..c18650796cc9e9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-worker.js b/test/fixtures/wpt/fetch/api/resources/keepalive-worker.js
new file mode 100644
index 00000000000000..0808601d0d95cb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/keepalive-worker.js
@@ -0,0 +1,15 @@
+/**
+* Script that sends keepalive
+* fetch request and terminates immediately.
+* The request URL is passed as a parameter to this worker
+*/
+function sendFetchRequest() {
+ // Parse the query parameter from the worker's script URL
+ const urlString = self.location.search.replace("?param=", "");
+ postMessage('started');
+ fetch(`${urlString}`, { keepalive: true });
+}
+
+sendFetchRequest();
+// Terminate the worker
+self.close();
diff --git a/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html b/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html
new file mode 100644
index 00000000000000..6e5d5065474d47
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js b/test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js
new file mode 100644
index 00000000000000..19d4b189d8594e
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js
@@ -0,0 +1,19 @@
+async function messageClient(clientId, message) {
+ const client = await clients.get(clientId);
+ client.postMessage(message);
+}
+
+addEventListener('fetch', event => {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+
+ function onAborted() {
+ messageClient(event.clientId, event.request.signal.reason);
+ resolve();
+ }
+
+ messageClient(event.clientId, 'fetch event has arrived');
+
+ event.respondWith(promise.then(() => new Response('hello')));
+ event.request.signal.addEventListener('abort', onAborted);
+});
diff --git a/test/fixtures/wpt/fetch/api/resources/sw-intercept.js b/test/fixtures/wpt/fetch/api/resources/sw-intercept.js
new file mode 100644
index 00000000000000..b8166b62a5c939
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/sw-intercept.js
@@ -0,0 +1,10 @@
+async function broadcast(msg) {
+ for (const client of await clients.matchAll()) {
+ client.postMessage(msg);
+ }
+}
+
+addEventListener('fetch', event => {
+ event.waitUntil(broadcast(event.request.url));
+ event.respondWith(fetch(event.request));
+});
diff --git a/test/fixtures/wpt/fetch/api/resources/top.txt b/test/fixtures/wpt/fetch/api/resources/top.txt
new file mode 100644
index 00000000000000..83a3157d14d908
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/top.txt
@@ -0,0 +1 @@
+top
\ No newline at end of file
diff --git a/test/fixtures/wpt/fetch/api/resources/utils.js b/test/fixtures/wpt/fetch/api/resources/utils.js
new file mode 100644
index 00000000000000..3721d9bf9cc7cf
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/resources/utils.js
@@ -0,0 +1,120 @@
+var RESOURCES_DIR = "../resources/";
+
+function dirname(path) {
+ return path.replace(/\/[^\/]*$/, '/')
+}
+
+function checkRequest(request, ExpectedValuesDict) {
+ for (var attribute in ExpectedValuesDict) {
+ switch(attribute) {
+ case "headers":
+ for (var key in ExpectedValuesDict["headers"].keys()) {
+ assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key),
+ "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key));
+ }
+ break;
+
+ case "body":
+ //for checking body's content, a dedicated asyncronous/promise test should be used
+ assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header")
+ break;
+
+ case "method":
+ case "referrer":
+ case "referrerPolicy":
+ case "credentials":
+ case "cache":
+ case "redirect":
+ case "integrity":
+ case "url":
+ case "destination":
+ assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute")
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+function stringToArray(str) {
+ var array = new Uint8Array(str.length);
+ for (var i=0, strLen = str.length; i < strLen; i++)
+ array[i] = str.charCodeAt(i);
+ return array;
+}
+
+function encode_utf8(str)
+{
+ if (self.TextEncoder)
+ return (new TextEncoder).encode(str);
+ return stringToArray(unescape(encodeURIComponent(str)));
+}
+
+function validateBufferFromString(buffer, expectedValue, message)
+{
+ return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message);
+}
+
+function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ return reader.read(new Uint8Array(64)).then(function(data) {
+ if (!data.done) {
+ assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
+ var newBuffer;
+ if (retrievedArrayBuffer) {
+ newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
+ newBuffer.set(retrievedArrayBuffer, 0);
+ newBuffer.set(data.value, retrievedArrayBuffer.length);
+ } else {
+ newBuffer = data.value;
+ }
+ return validateStreamFromString(reader, expectedValue, newBuffer);
+ }
+ validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream");
+ });
+}
+
+function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ return reader.read(new Uint8Array(64)).then(function(data) {
+ if (!data.done) {
+ assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
+ var newBuffer;
+ if (retrievedArrayBuffer) {
+ newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
+ newBuffer.set(retrievedArrayBuffer, 0);
+ newBuffer.set(data.value, retrievedArrayBuffer.length);
+ } else {
+ newBuffer = data.value;
+ }
+ return validateStreamFromPartialString(reader, expectedValue, newBuffer);
+ }
+
+ var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer);
+ return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream");
+ });
+}
+
+// From streams tests
+function delay(milliseconds)
+{
+ return new Promise(function(resolve) {
+ step_timeout(resolve, milliseconds);
+ });
+}
+
+function requestForbiddenHeaders(desc, forbiddenHeaders) {
+ var url = RESOURCES_DIR + "inspect-headers.py";
+ var requestInit = {"headers": forbiddenHeaders}
+ var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|");
+
+ promise_test(function(test){
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ for (var header in forbiddenHeaders)
+ assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined");
+ });
+ }, desc);
+}
diff --git a/test/fixtures/wpt/fetch/api/response/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/response/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..399d8c1669be60
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: fetch
+ files: "**"
diff --git a/test/fixtures/wpt/fetch/api/response/json.any.js b/test/fixtures/wpt/fetch/api/response/json.any.js
new file mode 100644
index 00000000000000..15f050e6324663
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/json.any.js
@@ -0,0 +1,14 @@
+// See also /xhr/json.any.js
+
+promise_test(async t => {
+ const response = await fetch(`data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`);
+ const json = await response.json();
+ assert_array_equals(Object.keys(json), ["b", "a"]);
+ assert_equals(json.a, 2);
+ assert_equals(json.b, 3);
+}, "Ensure the correct JSON parser is used");
+
+promise_test(async t => {
+ const response = await fetch("/xhr/resources/utf16-bom.json");
+ return promise_rejects_js(t, SyntaxError, response.json());
+}, "Ensure UTF-16 results in an error");
diff --git a/test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html b/test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html
new file mode 100644
index 00000000000000..fe5e7d4c0754a0
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html
@@ -0,0 +1,14 @@
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html b/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html
new file mode 100644
index 00000000000000..9bb6e0bbf3f8eb
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html
@@ -0,0 +1,3 @@
+
+Current page used as a test helper
+
diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html b/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html
new file mode 100644
index 00000000000000..f63372e64c2bef
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html
@@ -0,0 +1,16 @@
+
+Incumbent page used as a test helper
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html b/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html
new file mode 100644
index 00000000000000..44f42eda493c27
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html
@@ -0,0 +1,2 @@
+
+Relevant page used as a test helper
diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html b/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html
new file mode 100644
index 00000000000000..5f2f42a1cea52f
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html
@@ -0,0 +1,27 @@
+
+Response.redirect URL parsing, with multiple globals in play
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js b/test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js
new file mode 100644
index 00000000000000..19a5dfa5ff6e5d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js
@@ -0,0 +1,23 @@
+// META: title=realm of Response arrayBuffer()
+
+'use strict';
+
+promise_test(async () => {
+ await new Promise(resolve => {
+ onload = resolve;
+ });
+
+ let iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.srcdoc = '';
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ let otherRealm = iframe.contentWindow;
+
+ let ab = await window.Response.prototype.arrayBuffer.call(new otherRealm.Response(''));
+
+ assert_true(ab instanceof otherRealm.ArrayBuffer, "ArrayBuffer should be created in receiver's realm");
+ assert_false(ab instanceof ArrayBuffer, "ArrayBuffer should not be created in the arrayBuffer() methods's realm");
+}, 'realm of the ArrayBuffer from Response arrayBuffer()');
diff --git a/test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js b/test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js
new file mode 100644
index 00000000000000..1cc51fc71b6529
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js
@@ -0,0 +1,24 @@
+// META: global=window
+// META: title=realm of Response bytes()
+
+"use strict";
+
+promise_test(async () => {
+ await new Promise(resolve => {
+ onload = resolve;
+ });
+
+ let iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ iframe.srcdoc = "";
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ let otherRealm = iframe.contentWindow;
+
+ let ab = await window.Response.prototype.bytes.call(new otherRealm.Response(""));
+
+ assert_true(ab instanceof otherRealm.Uint8Array, "Uint8Array should be created in receiver's realm");
+ assert_false(ab instanceof Uint8Array, "Uint8Array should not be created in the bytes() methods's realm");
+}, "realm of the Uint8Array from Response bytes()");
diff --git a/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html b/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html
new file mode 100644
index 00000000000000..64b0755666168d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js
new file mode 100644
index 00000000000000..91140d1afd183b
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker
+// META: title=Response consume blob and http bodies
+// META: script=../resources/utils.js
+
+promise_test(function(test) {
+ return new Response(new Blob([], { "type" : "text/plain" })).body.cancel();
+}, "Cancelling a starting blob Response stream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob(["This is data"], { "type" : "text/plain" }));
+ var reader = response.body.getReader();
+ reader.read();
+ return reader.cancel();
+}, "Cancelling a loading blob Response stream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob(["T"], { "type" : "text/plain" }));
+ var reader = response.body.getReader();
+
+ var closedPromise = reader.closed.then(function() {
+ return reader.cancel();
+ });
+ reader.read().then(function readMore({done, value}) {
+ if (!done) return reader.read().then(readMore);
+ });
+ return closedPromise;
+}, "Cancelling a closed blob Response stream");
+
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) {
+ return response.body.cancel();
+ });
+}, "Cancelling a starting Response stream");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) {
+ var reader = response.body.getReader();
+ return reader.read().then(function() {
+ return reader.cancel();
+ });
+ });
+}, "Cancelling a loading Response stream");
+
+promise_test(function() {
+ async function readAll(reader) {
+ while (true) {
+ const {value, done} = await reader.read();
+ if (done)
+ return;
+ }
+ }
+
+ return fetch(RESOURCES_DIR + "top.txt").then(function(response) {
+ var reader = response.body.getReader();
+ return readAll(reader).then(() => reader.cancel());
+ });
+}, "Cancelling a closed Response stream");
+
+promise_test(async () => {
+ const response = await fetch(RESOURCES_DIR + "top.txt");
+ const { body } = response;
+ await body.cancel();
+ assert_equals(body, response.body, ".body should not change after cancellation");
+}, "Accessing .body after canceling it");
diff --git a/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js b/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js
new file mode 100644
index 00000000000000..da54616c376d91
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js
@@ -0,0 +1,32 @@
+// Verify that calling Response clone() in a detached iframe doesn't crash.
+// Regression test for https://crbug.com/1082688.
+
+'use strict';
+
+promise_test(async () => {
+ // Wait for the document body to be available.
+ await new Promise(resolve => {
+ onload = resolve;
+ });
+
+ window.iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.srcdoc = `
+
+`;
+
+ await new Promise(resolve => {
+ onmessage = evt => {
+ if (evt.data === 'okay') {
+ resolve();
+ }
+ };
+ });
+
+ // If it got here without crashing, the test passed.
+}, 'clone within removed iframe should not crash');
diff --git a/test/fixtures/wpt/fetch/api/response/response-clone.any.js b/test/fixtures/wpt/fetch/api/response/response-clone.any.js
new file mode 100644
index 00000000000000..20ce01e9997163
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-clone.any.js
@@ -0,0 +1,141 @@
+// META: global=window,worker
+// META: title=Response clone
+// META: script=../resources/utils.js
+
+var defaultValues = { "type" : "default",
+ "url" : "",
+ "ok" : true,
+ "status" : 200,
+ "statusText" : ""
+};
+
+var response = new Response();
+var clonedResponse = response.clone();
+test(function() {
+ for (var attributeName in defaultValues) {
+ var expectedValue = defaultValues[attributeName];
+ assert_equals(clonedResponse[attributeName], expectedValue,
+ "Expect default response." + attributeName + " is " + expectedValue);
+ }
+}, "Check Response's clone with default values, without body");
+
+var body = "This is response body";
+var headersInit = { "name" : "value" };
+var responseInit = { "status" : 200,
+ "statusText" : "GOOD",
+ "headers" : headersInit
+};
+var response = new Response(body, responseInit);
+var clonedResponse = response.clone();
+test(function() {
+ assert_equals(clonedResponse.status, responseInit["status"],
+ "Expect response.status is " + responseInit["status"]);
+ assert_equals(clonedResponse.statusText, responseInit["statusText"],
+ "Expect response.statusText is " + responseInit["statusText"]);
+ assert_equals(clonedResponse.headers.get("name"), "value",
+ "Expect response.headers has name:value header");
+}, "Check Response's clone has the expected attribute values");
+
+promise_test(function(test) {
+ return validateStreamFromString(response.body.getReader(), body);
+}, "Check original response's body after cloning");
+
+promise_test(function(test) {
+ return validateStreamFromString(clonedResponse.body.getReader(), body);
+}, "Check cloned response's body");
+
+promise_test(function(test) {
+ var disturbedResponse = new Response("data");
+ return disturbedResponse.text().then(function() {
+ assert_true(disturbedResponse.bodyUsed, "response is disturbed");
+ assert_throws_js(TypeError, function() { disturbedResponse.clone(); },
+ "Expect TypeError exception");
+ });
+}, "Cannot clone a disturbed response");
+
+promise_test(function(t) {
+ var clone;
+ var result;
+ var response;
+ return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) {
+ clone = res.clone();
+ response = res;
+ return clone.text();
+ }).then(function(r) {
+ assert_equals(r.length, 26);
+ result = r;
+ return response.text();
+ }).then(function(r) {
+ assert_equals(r, result, "cloned responses should provide the same data");
+ });
+ }, 'Cloned responses should provide the same data');
+
+promise_test(function(t) {
+ var clone;
+ return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) {
+ clone = res.clone();
+ res.body.cancel();
+ assert_true(res.bodyUsed);
+ assert_false(clone.bodyUsed);
+ return clone.arrayBuffer();
+ }).then(function(r) {
+ assert_equals(r.byteLength, 26);
+ assert_true(clone.bodyUsed);
+ });
+}, 'Cancelling stream should not affect cloned one');
+
+function testReadableStreamClone(initialBuffer, bufferType)
+{
+ promise_test(function(test) {
+ var response = new Response(new ReadableStream({start : function(controller) {
+ controller.enqueue(initialBuffer);
+ controller.close();
+ }}));
+
+ var clone = response.clone();
+ var stream1 = response.body;
+ var stream2 = clone.body;
+
+ var buffer;
+ return stream1.getReader().read().then(function(data) {
+ assert_false(data.done);
+ assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer");
+ return stream2.getReader().read();
+ }).then(function(data) {
+ assert_false(data.done);
+ if (initialBuffer instanceof ArrayBuffer) {
+ assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBuffer");
+ assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal");
+ assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content");
+ } else if (initialBuffer instanceof DataView) {
+ assert_true(data.value instanceof DataView, "Cloned buffer is DataView");
+ assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal");
+ assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal");
+ for (let i = 0; i < initialBuffer.byteLength; ++i) {
+ assert_equals(
+ data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}");
+ }
+ } else {
+ assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content");
+ }
+ assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type");
+ assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer");
+ });
+ }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)");
+}
+
+var arrayBuffer = new ArrayBuffer(16);
+testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array");
+testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array");
+testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array");
+testReadableStreamClone(arrayBuffer, "ArrayBuffer");
+testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array");
+testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray");
+testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array");
+testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array");
+testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array");
+testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array");
+testReadableStreamClone(typeof Float16Array === "function" ? new Float16Array(arrayBuffer) : undefined, "Float16Array");
+testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array");
+testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array");
+testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView");
diff --git a/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js b/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js
new file mode 100644
index 00000000000000..a5df3562586589
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js
@@ -0,0 +1,88 @@
+// META: global=window,worker
+// META: title=Response consume empty bodies
+
+function checkBodyText(test, response) {
+ return response.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, "", "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+}
+
+async function checkBodyBlob(test, response) {
+ const bodyAsBlob = await response.blob();
+ const body = await bodyAsBlob.text();
+
+ assert_equals(body, "", "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+}
+
+function checkBodyArrayBuffer(test, response) {
+ return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyJSON(test, response) {
+ return response.json().then(
+ function(bodyAsJSON) {
+ assert_unreached("JSON parsing should fail");
+ },
+ function() {
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyFormData(test, response) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyFormDataError(test, response) {
+ return promise_rejects_js(test, TypeError, response.formData()).then(function() {
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkResponseWithNoBody(bodyType, checkFunction, headers = []) {
+ promise_test(function(test) {
+ var response = new Response(undefined, { "headers": headers });
+ assert_false(response.bodyUsed);
+ return checkFunction(test, response);
+ }, "Consume response's body as " + bodyType);
+}
+
+checkResponseWithNoBody("text", checkBodyText);
+checkResponseWithNoBody("blob", checkBodyBlob);
+checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer);
+checkResponseWithNoBody("json (error case)", checkBodyJSON);
+checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
+checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
+checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
+
+function checkResponseWithEmptyBody(bodyType, body, asText) {
+ promise_test(function(test) {
+ var response = new Response(body);
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ if (asText) {
+ return response.text().then(function(bodyAsString) {
+ assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
+ assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }
+ return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer"));
+}
+
+checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
+checkResponseWithEmptyBody("text", "", false);
+checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
+checkResponseWithEmptyBody("text", "", true);
+checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
+checkResponseWithEmptyBody("FormData", new FormData(), true);
+checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);
diff --git a/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js
new file mode 100644
index 00000000000000..f89d7341ac6ca9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js
@@ -0,0 +1,80 @@
+// META: global=window,worker
+// META: title=Response consume
+// META: script=../resources/utils.js
+
+promise_test(function(test) {
+ var body = "";
+ var response = new Response("");
+ return validateStreamFromString(response.body.getReader(), "");
+}, "Read empty text response's body as readableStream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob([], { "type" : "text/plain" }));
+ return validateStreamFromString(response.body.getReader(), "");
+}, "Read empty blob response's body as readableStream");
+
+var formData = new FormData();
+formData.append("name", "value");
+var textData = JSON.stringify("This is response's body");
+var blob = new Blob([textData], { "type" : "text/plain" });
+var urlSearchParamsData = "name=value";
+var urlSearchParams = new URLSearchParams(urlSearchParamsData);
+
+for (const mode of [undefined, "byob"]) {
+ promise_test(function(test) {
+ var response = new Response(blob);
+ return validateStreamFromString(response.body.getReader({ mode }), textData);
+ }, `Read blob response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(textData);
+ return validateStreamFromString(response.body.getReader({ mode }), textData);
+ }, `Read text response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(urlSearchParams);
+ return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData);
+ }, `Read URLSearchParams response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var arrayBuffer = new ArrayBuffer(textData.length);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < textData.length; cptr++)
+ int8Array[cptr] = textData.charCodeAt(cptr);
+
+ return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData);
+ }, `Read array buffer response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(formData);
+ return validateStreamFromPartialString(response.body.getReader({ mode }),
+ "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue");
+ }, `Read form data response's body as readableStream with mode=${mode}`);
+}
+
+test(function() {
+ assert_equals(Response.error().body, null);
+}, "Getting an error Response stream");
+
+test(function() {
+ assert_equals(Response.redirect("/").body, null);
+}, "Getting a redirect Response stream");
+
+promise_test(async function(test) {
+ var buffer = new ArrayBuffer(textData.length);
+
+ var body = new Response(textData).body;
+ const reader = body.getReader( {mode: 'byob'} );
+
+ let offset = 3;
+ while (offset < textData.length) {
+ const {done, value} = await reader.read(new Uint8Array(buffer, offset));
+ if (done) {
+ break;
+ }
+ buffer = value.buffer;
+ offset += value.byteLength;
+ }
+
+ validateBufferFromString(buffer, `\0\0\0\"This is response's bo`, 'Buffer should be validated');
+}, `Reading with offset from Response stream`);
diff --git a/test/fixtures/wpt/fetch/api/response/response-consume.html b/test/fixtures/wpt/fetch/api/response/response-consume.html
new file mode 100644
index 00000000000000..89fc49fd3c2b11
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-consume.html
@@ -0,0 +1,317 @@
+
+
+
+
+ Response consume
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js
new file mode 100644
index 00000000000000..33cad40e757bde
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Response Receives Propagated Error from ReadableStream
+
+function newStreamWithStartError() {
+ var err = new Error("Start error");
+ return [new ReadableStream({
+ start(controller) {
+ controller.error(err);
+ }
+ }),
+ err]
+}
+
+function newStreamWithPullError() {
+ var err = new Error("Pull error");
+ return [new ReadableStream({
+ pull(controller) {
+ controller.error(err);
+ }
+ }),
+ err]
+}
+
+function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) {
+ promise_test(test => {
+ return promise_rejects_exactly(
+ test,
+ err,
+ new Response(stream)[responseReaderMethod](),
+ 'CustomTestError should propagate'
+ )
+ }, testDescription)
+}
+
+
+promise_test(test => {
+ var [stream, err] = newStreamWithStartError();
+ return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate')
+}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error")
+
+promise_test(test => {
+ var [stream, err] = newStreamWithPullError();
+ return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate')
+}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error")
+
+
+// test start() errors for all Body reader methods
+runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'bytes', 'ReadableStream start() Error propagates to Response.bytes() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise');
+
+// test pull() errors for all Body reader methods
+runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'bytes', 'ReadableStream pull() Error propagates to Response.bytes() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise');
diff --git a/test/fixtures/wpt/fetch/api/response/response-error.any.js b/test/fixtures/wpt/fetch/api/response/response-error.any.js
new file mode 100644
index 00000000000000..a76bc4380286fa
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-error.any.js
@@ -0,0 +1,27 @@
+// META: global=window,worker
+// META: title=Response error
+
+var invalidStatus = [0, 100, 199, 600, 1000];
+invalidStatus.forEach(function(status) {
+ test(function() {
+ assert_throws_js(RangeError, function() { new Response("", { "status" : status }); },
+ "Expect RangeError exception when status is " + status);
+ },"Throws RangeError when responseInit's status is " + status);
+});
+
+var invalidStatusText = ["\n", "Ā"];
+invalidStatusText.forEach(function(statusText) {
+ test(function() {
+ assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); },
+ "Expect TypeError exception " + statusText);
+ },"Throws TypeError when responseInit's statusText is " + statusText);
+});
+
+var nullBodyStatus = [204, 205, 304];
+nullBodyStatus.forEach(function(status) {
+ test(function() {
+ assert_throws_js(TypeError,
+ function() { new Response("body", {"status" : status }); },
+ "Expect TypeError exception ");
+ },"Throws TypeError when building a response with body and a body status of " + status);
+});
diff --git a/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js
new file mode 100644
index 00000000000000..ea5192bfb10dcf
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js
@@ -0,0 +1,23 @@
+// META: global=window,worker
+
+"use strict";
+
+test(() => {
+ const stream = new ReadableStream();
+ stream.getReader();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which getReader() is called");
+
+test(() => {
+ const stream = new ReadableStream();
+ stream.getReader().read();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which read() is called");
+
+promise_test(async () => {
+ const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }),
+ reader = stream.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which read() and releaseLock() are called");
diff --git a/test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js b/test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js
new file mode 100644
index 00000000000000..4a67d067a71850
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js
@@ -0,0 +1,8 @@
+// META: global=window,worker
+// META: title=Response: error static method
+
+promise_test (async () => {
+ const response = await fetch("../resources/data.json");
+ assert_throws_js(TypeError, () => { response.headers.append("name", "value"); });
+ assert_not_equals(response.headers.get("name"), "value", "response headers should be immutable");
+}, "Ensure response headers are immutable");
diff --git a/test/fixtures/wpt/fetch/api/response/response-init-001.any.js b/test/fixtures/wpt/fetch/api/response/response-init-001.any.js
new file mode 100644
index 00000000000000..559e49ad11ffe1
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-init-001.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker
+// META: title=Response init: simple cases
+
+var defaultValues = { "type" : "default",
+ "url" : "",
+ "ok" : true,
+ "status" : 200,
+ "statusText" : "",
+ "body" : null
+};
+
+var statusCodes = { "givenValues" : [200, 300, 400, 500, 599],
+ "expectedValues" : [200, 300, 400, 500, 599]
+};
+var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)],
+ "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)]
+};
+var initValuesDict = { "status" : statusCodes,
+ "statusText" : statusTexts
+};
+
+function isOkStatus(status) {
+ return 200 <= status && 299 >= status;
+}
+
+var response = new Response();
+for (var attributeName in defaultValues) {
+ test(function() {
+ var expectedValue = defaultValues[attributeName];
+ assert_equals(response[attributeName], expectedValue,
+ "Expect default response." + attributeName + " is " + expectedValue);
+ }, "Check default value for " + attributeName + " attribute");
+}
+
+for (var attributeName in initValuesDict) {
+ test(function() {
+ var valuesToTest = initValuesDict[attributeName];
+ for (var valueIdx in valuesToTest["givenValues"]) {
+ var givenValue = valuesToTest["givenValues"][valueIdx];
+ var expectedValue = valuesToTest["expectedValues"][valueIdx];
+ var responseInit = {};
+ responseInit[attributeName] = givenValue;
+ var response = new Response("", responseInit);
+ assert_equals(response[attributeName], expectedValue,
+ "Expect response." + attributeName + " is " + expectedValue +
+ " when initialized with " + givenValue);
+ assert_equals(response.ok, isOkStatus(response.status),
+ "Expect response.ok is " + isOkStatus(response.status));
+ }
+ }, "Check " + attributeName + " init values and associated getter");
+}
+
+test(function() {
+ const response1 = new Response("");
+ assert_equals(response1.headers, response1.headers);
+
+ const response2 = new Response("", {"headers": {"X-Foo": "bar"}});
+ assert_equals(response2.headers, response2.headers);
+ const headers = response2.headers;
+ response2.headers.set("X-Foo", "quux");
+ assert_equals(headers, response2.headers);
+ headers.set("X-Other-Header", "baz");
+ assert_equals(headers, response2.headers);
+}, "Test that Response.headers has the [SameObject] extended attribute");
diff --git a/test/fixtures/wpt/fetch/api/response/response-init-002.any.js b/test/fixtures/wpt/fetch/api/response/response-init-002.any.js
new file mode 100644
index 00000000000000..6c0a46e480406c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-init-002.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Response init: body and headers
+// META: script=../resources/utils.js
+
+test(function() {
+ var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3"
+ };
+ var headers = new Headers(headerDict);
+ var response = new Response("", { "headers" : headers })
+ for (var name in headerDict) {
+ assert_equals(response.headers.get(name), headerDict[name],
+ "response's headers has " + name + " : " + headerDict[name]);
+ }
+}, "Initialize Response with headers values");
+
+function checkResponseInit(body, bodyType, expectedTextBody) {
+ promise_test(function(test) {
+ var response = new Response(body);
+ var resHeaders = response.headers;
+ var mime = resHeaders.get("Content-Type");
+ assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" ");
+ return response.text().then(function(bodyAsText) {
+ //not equals: cannot guess formData exact value
+ assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body");
+ });
+ }, "Initialize Response's body with " + bodyType);
+}
+
+var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
+var formaData = new FormData();
+formaData.append("name", "value");
+var urlSearchParams = "URLSearchParams are not supported";
+//avoid test timeout if not implemented
+if (self.URLSearchParams)
+ urlSearchParams = new URLSearchParams("name=value");
+var usvString = "This is a USVString"
+
+checkResponseInit(blob, "application/octet-binary", "This is a blob");
+checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
+checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value");
+checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+
+promise_test(function(test) {
+ var body = "This is response body";
+ var response = new Response(body);
+ return validateStreamFromString(response.body.getReader(), body);
+}, "Read Response's body as readableStream");
+
+promise_test(function(test) {
+ var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]});
+ return response.blob().then(function(blob) {
+ assert_equals(blob.type, "", "Blob type should be the empty string");
+ });
+}, "Testing empty Response Content-Type header");
+
+test(function() {
+ var response = new Response(null, {status: 204});
+ assert_equals(response.body, null);
+}, "Testing null Response body");
diff --git a/test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js b/test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js
new file mode 100644
index 00000000000000..3a7744c28782c8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js
@@ -0,0 +1,125 @@
+test(() => {
+ const response = new Response();
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), "a/b; c=d");
+}, "Default Content-Type for Response with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const response = new Response(buffer);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with buffer source body");
+
+promise_test(async () => {
+ const formData = new FormData();
+ formData.append("a", "b");
+ const response = new Response(formData);
+ const boundary = (await response.text()).split("\r\n")[0].slice(2);
+ assert_equals(
+ response.headers.get("Content-Type"),
+ `multipart/form-data; boundary=${boundary}`,
+ );
+}, "Default Content-Type for Response with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const response = new Response(usp);
+ assert_equals(
+ response.headers.get("Content-Type"),
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ );
+}, "Default Content-Type for Response with URLSearchParams body");
+
+test(() => {
+ const response = new Response("");
+ assert_equals(
+ response.headers.get("Content-Type"),
+ "text/plain;charset=UTF-8",
+ );
+}, "Default Content-Type for Response with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with ReadableStream body");
+
+// -----------------------------------------------------------------------------
+
+const OVERRIDE_MIME = "test/only; mime=type";
+
+function responseWithOverrideMime(body) {
+ return new Response(
+ body,
+ { headers: { "Content-Type": OVERRIDE_MIME } },
+ );
+}
+
+test(() => {
+ const response = responseWithOverrideMime(undefined);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const response = responseWithOverrideMime(buffer);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with buffer source body");
+
+test(() => {
+ const formData = new FormData();
+ const response = responseWithOverrideMime(formData);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const response = responseWithOverrideMime(usp);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with URLSearchParams body");
+
+test(() => {
+ const response = responseWithOverrideMime("");
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = responseWithOverrideMime(stream);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with ReadableStream body");
diff --git a/test/fixtures/wpt/fetch/api/response/response-static-error.any.js b/test/fixtures/wpt/fetch/api/response/response-static-error.any.js
new file mode 100644
index 00000000000000..4097eab37b4c90
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-static-error.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+// META: title=Response: error static method
+
+test(function() {
+ var responseError = Response.error();
+ assert_equals(responseError.type, "error", "Network error response's type is error");
+ assert_equals(responseError.status, 0, "Network error response's status is 0");
+ assert_equals(responseError.statusText, "", "Network error response's statusText is empty");
+ assert_equals(responseError.body, null, "Network error response's body is null");
+
+ assert_true(responseError.headers.entries().next().done, "Headers should be empty");
+}, "Check response returned by static method error()");
+
+test(function() {
+ const headers = Response.error().headers;
+
+ // Avoid false positives if expected API is not available
+ assert_true(!!headers);
+ assert_equals(typeof headers.append, 'function');
+
+ assert_throws_js(TypeError, function () { headers.append('name', 'value'); });
+}, "the 'guard' of the Headers instance should be immutable");
diff --git a/test/fixtures/wpt/fetch/api/response/response-static-json.any.js b/test/fixtures/wpt/fetch/api/response/response-static-json.any.js
new file mode 100644
index 00000000000000..5ec79e69aa3f9c
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-static-json.any.js
@@ -0,0 +1,96 @@
+// META: global=window,worker
+// META: title=Response: json static method
+
+const APPLICATION_JSON = "application/json";
+const FOO_BAR = "foo/bar";
+
+const INIT_TESTS = [
+ [undefined, 200, "", APPLICATION_JSON, {}],
+ [{ status: 400 }, 400, "", APPLICATION_JSON, {}],
+ [{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}],
+ [{ headers: {} }, 200, "", APPLICATION_JSON, {}],
+ [{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}],
+ [{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }],
+];
+
+for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) {
+ promise_test(async function () {
+ const response = Response.json("hello world", init);
+ assert_equals(response.type, "default", "Response's type is default");
+ assert_equals(response.status, expectedStatus, "Response's status is " + expectedStatus);
+ assert_equals(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText));
+ assert_equals(response.headers.get("content-type"), expectedContentType, "Response's content-type is " + expectedContentType);
+ for (const key in expectedHeaders) {
+ assert_equals(response.headers.get(key), expectedHeaders[key], "Response's header " + key + " is " + JSON.stringify(expectedHeaders[key]));
+ }
+
+ const data = await response.json();
+ assert_equals(data, "hello world", "Response's body is 'hello world'");
+ }, `Check response returned by static json() with init ${JSON.stringify(init)}`);
+}
+
+const nullBodyStatus = [204, 205, 304];
+for (const status of nullBodyStatus) {
+ test(function () {
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json("hello world", { status: status });
+ },
+ );
+ }, `Throws TypeError when calling static json() with a status of ${status}`);
+}
+
+promise_test(async function () {
+ const response = Response.json({ foo: "bar" });
+ const data = await response.json();
+ assert_equals(typeof data, "object", "Response's json body is an object");
+ assert_equals(data.foo, "bar", "Response's json body is { foo: 'bar' }");
+}, "Check static json() encodes JSON objects correctly");
+
+test(function () {
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json(Symbol("foo"));
+ },
+ );
+}, "Check static json() throws when data is not encodable");
+
+test(function () {
+ const a = { b: 1 };
+ a.a = a;
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json(a);
+ },
+ );
+}, "Check static json() throws when data is circular");
+
+promise_test(async function () {
+ class CustomError extends Error {
+ name = "CustomError";
+ }
+ assert_throws_js(
+ CustomError,
+ function () {
+ Response.json({ get foo() { throw new CustomError("bar") }});
+ }
+ )
+}, "Check static json() propagates JSON serializer errors");
+
+const encodingChecks = [
+ ["𝌆", [34, 240, 157, 140, 134, 34]],
+ ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]],
+ ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]],
+];
+
+for (const [input, expected] of encodingChecks) {
+ promise_test(async function () {
+ const response = Response.json(input);
+ const buffer = await response.arrayBuffer();
+ const data = new Uint8Array(buffer);
+ assert_array_equals(data, expected);
+ }, `Check response returned by static json() with input ${input}`);
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js b/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js
new file mode 100644
index 00000000000000..b16c56d83003d9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js
@@ -0,0 +1,40 @@
+// META: global=window,worker
+// META: title=Response: redirect static method
+
+var url = "http://test.url:1234/";
+test(function() {
+ const redirectResponse = Response.redirect(url);
+ assert_equals(redirectResponse.type, "default");
+ assert_false(redirectResponse.redirected);
+ assert_false(redirectResponse.ok);
+ assert_equals(redirectResponse.status, 302, "Default redirect status is 302");
+ assert_equals(redirectResponse.headers.get("Location"), url,
+ "redirected response has Location header with the correct url");
+ assert_equals(redirectResponse.statusText, "");
+}, "Check default redirect response");
+
+[301, 302, 303, 307, 308].forEach(function(status) {
+ test(function() {
+ const redirectResponse = Response.redirect(url, status);
+ assert_equals(redirectResponse.type, "default");
+ assert_false(redirectResponse.redirected);
+ assert_false(redirectResponse.ok);
+ assert_equals(redirectResponse.status, status, "Redirect status is " + status);
+ assert_equals(redirectResponse.headers.get("Location"), url);
+ assert_equals(redirectResponse.statusText, "");
+ }, "Check response returned by static method redirect(), status = " + status);
+});
+
+test(function() {
+ var invalidUrl = "http://:This is not an url";
+ assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); },
+ "Expect TypeError exception");
+}, "Check error returned when giving invalid url to redirect()");
+
+var invalidRedirectStatus = [200, 309, 400, 500];
+invalidRedirectStatus.forEach(function(invalidStatus) {
+ test(function() {
+ assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); },
+ "Expect RangeError exception");
+ }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus);
+});
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js
new file mode 100644
index 00000000000000..8e83cd190873a9
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js
@@ -0,0 +1,25 @@
+// META: global=window,worker
+// META: title=Response causes TypeError from bad chunk type
+
+function runChunkTest(responseReaderMethod, testDescription) {
+ promise_test(test => {
+ let stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue("not Uint8Array");
+ controller.close();
+ }
+ });
+
+ return promise_rejects_js(test, TypeError,
+ new Response(stream)[responseReaderMethod](),
+ 'TypeError should propagate'
+ )
+ }, testDescription)
+}
+
+runChunkTest('arrayBuffer', 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError');
+runChunkTest('blob', 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError');
+runChunkTest('bytes', 'ReadableStream with non-Uint8Array chunk passed to Response.bytes() causes TypeError');
+runChunkTest('formData', 'ReadableStream with non-Uint8Array chunk passed to Response.formData() causes TypeError');
+runChunkTest('json', 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError');
+runChunkTest('text', 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError');
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js
new file mode 100644
index 00000000000000..64f65f16f23e7d
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js
@@ -0,0 +1,44 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ const reader = response.body.getReader();
+ reader.releaseLock();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.blob().then(function(blob) {
+ assert_true(blob instanceof Blob);
+ });
+ });
+ }, `Getting blob after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.text().then(function(text) {
+ assert_true(text.length > 0);
+ });
+ });
+ }, `Getting text after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.json().then(function(json) {
+ assert_equals(typeof json, "object");
+ });
+ });
+ }, `Getting json after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.arrayBuffer().then(function(arrayBuffer) {
+ assert_true(arrayBuffer.byteLength > 0);
+ });
+ });
+ }, `Getting arrayBuffer after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js
new file mode 100644
index 00000000000000..c46a180a18d794
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js
@@ -0,0 +1,35 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithLockedReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ response.body.getReader();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after getting a locked Response body (body source: ${bodySource})`);
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js
new file mode 100644
index 00000000000000..35fb086469b440
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js
@@ -0,0 +1,36 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithDisturbedReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ const reader = response.body.getReader();
+ reader.read();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after reading the Response body (body source: ${bodySource})`);
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js
new file mode 100644
index 00000000000000..490672febd0254
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js
@@ -0,0 +1,35 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithCancelledReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ response.body.cancel();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after cancelling the Response body (body source: ${bodySource})`);
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js
new file mode 100644
index 00000000000000..348fc3938314dc
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js
@@ -0,0 +1,19 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ for (const consumeAs of ["blob", "text", "json", "arrayBuffer"]) {
+ promise_test(
+ async () => {
+ const response = await responseFromBodySource(bodySource);
+ response[consumeAs]();
+ assert_not_equals(response.body, null);
+ assert_throws_js(TypeError, function () {
+ response.body.getReader();
+ });
+ },
+ `Getting a body reader after consuming as ${consumeAs} (body source: ${bodySource})`,
+ );
+ }
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js
new file mode 100644
index 00000000000000..61d8544f0786c8
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js
@@ -0,0 +1,76 @@
+// META: global=window,worker
+// META: title=ReadableStream disturbed tests, via Response's bodyUsed property
+
+"use strict";
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read();
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "A non-closed stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.cancel();
+ assert_true(response.bodyUsed, "After calling stream.cancel()");
+}, "A non-closed stream on which cancel() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read();
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "A closed stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.error(new Error("some error"));
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read().then(() => { }, () => { });
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "An errored stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.error(new Error("some error"));
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.cancel().then(() => { }, () => { });
+ assert_true(response.bodyUsed, "After calling stream.cancel()");
+}, "An errored stream on which cancel() has been called");
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js
new file mode 100644
index 00000000000000..5341b75271ead5
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+test(() => {
+ const r = new Response(new ReadableStream());
+ // highWaterMark: 0 means that nothing will actually be read from the body.
+ r.body.pipeTo(new WritableStream({}, {highWaterMark: 0}));
+ assert_true(r.bodyUsed, 'bodyUsed should be true');
+}, 'using pipeTo on Response body should disturb it synchronously');
+
+test(() => {
+ const r = new Response(new ReadableStream());
+ r.body.pipeThrough({
+ writable: new WritableStream({}, {highWaterMark: 0}),
+ readable: new ReadableStream()
+ });
+ assert_true(r.bodyUsed, 'bodyUsed should be true');
+}, 'using pipeThrough on Response body should disturb it synchronously');
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js
new file mode 100644
index 00000000000000..50bb586aa07439
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js
@@ -0,0 +1,17 @@
+const BODY = '{"key": "value"}';
+
+function responseFromBodySource(bodySource) {
+ if (bodySource === "fetch") {
+ return fetch("../resources/data.json");
+ } else if (bodySource === "stream") {
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode(BODY));
+ controller.close();
+ },
+ });
+ return new Response(stream);
+ } else {
+ return new Response(BODY);
+ }
+}
diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js
new file mode 100644
index 00000000000000..8fef66c8a281c6
--- /dev/null
+++ b/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js
@@ -0,0 +1,117 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+promise_test(async () => {
+ // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so
+ // these tests use add_completion_callback for cleanup instead.
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const bye = new TextEncoder().encode('bye');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: bye});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: undefined});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject value: undefined via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled(undefined);
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject undefined via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled(8.2);
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject 8.2 via Object.prototype.then.');
+
+promise_test(async () => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const bye = new TextEncoder().encode('bye');
+ const resp = new Response(hello);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: bye});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' +
+ 'should not be possible');
+
+promise_test(async () => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const u8a123 = new Uint8Array([1, 2, 3]);
+ const u8a456 = new Uint8Array([4, 5, 6]);
+ const resp = new Response(u8a123);
+ const writtenBytes = [];
+ const ws = new WritableStream({
+ write(chunk) {
+ writtenBytes.push(...Array.from(chunk));
+ }
+ });
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: u8a456});
+ };
+ await resp.body.pipeTo(ws);
+ delete Object.prototype.then;
+ assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]');
+}, 'intercepting arraybuffer to body readable stream conversion via ' +
+ 'Object.prototype.then should not be possible');
diff --git a/test/fixtures/wpt/interfaces/dom.idl b/test/fixtures/wpt/interfaces/dom.idl
index 253e7bf913eba9..1ddc084b949df6 100644
--- a/test/fixtures/wpt/interfaces/dom.idl
+++ b/test/fixtures/wpt/interfaces/dom.idl
@@ -313,7 +313,7 @@ interface Document : Node {
interface XMLDocument : Document {};
dictionary ElementCreationOptions {
- CustomElementRegistry customElementRegistry;
+ CustomElementRegistry? customElementRegistry;
DOMString is;
};
@@ -324,7 +324,7 @@ dictionary ImportNodeOptions {
[Exposed=Window]
interface DOMImplementation {
- [NewObject] DocumentType createDocumentType(DOMString qualifiedName, DOMString publicId, DOMString systemId);
+ [NewObject] DocumentType createDocumentType(DOMString name, DOMString publicId, DOMString systemId);
[NewObject] XMLDocument createDocument(DOMString? namespace, [LegacyNullToEmptyString] DOMString qualifiedName, optional DocumentType? doctype = null);
[NewObject] Document createHTMLDocument(optional DOMString title);
@@ -375,8 +375,8 @@ interface Element : Node {
sequence getAttributeNames();
DOMString? getAttribute(DOMString qualifiedName);
DOMString? getAttributeNS(DOMString? namespace, DOMString localName);
- [CEReactions] undefined setAttribute(DOMString qualifiedName, DOMString value);
- [CEReactions] undefined setAttributeNS(DOMString? namespace, DOMString qualifiedName, DOMString value);
+ [CEReactions] undefined setAttribute(DOMString qualifiedName, (TrustedType or DOMString) value);
+ [CEReactions] undefined setAttributeNS(DOMString? namespace, DOMString qualifiedName, (TrustedType or DOMString) value);
[CEReactions] undefined removeAttribute(DOMString qualifiedName);
[CEReactions] undefined removeAttributeNS(DOMString? namespace, DOMString localName);
[CEReactions] boolean toggleAttribute(DOMString qualifiedName, optional boolean force);
@@ -412,7 +412,7 @@ dictionary ShadowRootInit {
SlotAssignmentMode slotAssignment = "named";
boolean clonable = false;
boolean serializable = false;
- CustomElementRegistry customElementRegistry;
+ CustomElementRegistry? customElementRegistry;
};
[Exposed=Window,
diff --git a/test/fixtures/wpt/interfaces/fetch.idl b/test/fixtures/wpt/interfaces/fetch.idl
new file mode 100644
index 00000000000000..3d60842c4828ee
--- /dev/null
+++ b/test/fixtures/wpt/interfaces/fetch.idl
@@ -0,0 +1,132 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Fetch Standard (https://fetch.spec.whatwg.org/)
+
+typedef (sequence> or record) HeadersInit;
+
+[Exposed=(Window,Worker)]
+interface Headers {
+ constructor(optional HeadersInit init);
+
+ undefined append(ByteString name, ByteString value);
+ undefined delete(ByteString name);
+ ByteString? get(ByteString name);
+ sequence getSetCookie();
+ boolean has(ByteString name);
+ undefined set(ByteString name, ByteString value);
+ iterable;
+};
+
+typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) XMLHttpRequestBodyInit;
+
+typedef (ReadableStream or XMLHttpRequestBodyInit) BodyInit;
+interface mixin Body {
+ readonly attribute ReadableStream? body;
+ readonly attribute boolean bodyUsed;
+ [NewObject] Promise arrayBuffer();
+ [NewObject] Promise blob();
+ [NewObject] Promise bytes();
+ [NewObject] Promise formData();
+ [NewObject] Promise json();
+ [NewObject] Promise text();
+};
+typedef (Request or USVString) RequestInfo;
+
+[Exposed=(Window,Worker)]
+interface Request {
+ constructor(RequestInfo input, optional RequestInit init = {});
+
+ readonly attribute ByteString method;
+ readonly attribute USVString url;
+ [SameObject] readonly attribute Headers headers;
+
+ readonly attribute RequestDestination destination;
+ readonly attribute USVString referrer;
+ readonly attribute ReferrerPolicy referrerPolicy;
+ readonly attribute RequestMode mode;
+ readonly attribute RequestCredentials credentials;
+ readonly attribute RequestCache cache;
+ readonly attribute RequestRedirect redirect;
+ readonly attribute DOMString integrity;
+ readonly attribute boolean keepalive;
+ readonly attribute boolean isReloadNavigation;
+ readonly attribute boolean isHistoryNavigation;
+ readonly attribute AbortSignal signal;
+ readonly attribute RequestDuplex duplex;
+
+ [NewObject] Request clone();
+};
+Request includes Body;
+
+dictionary RequestInit {
+ ByteString method;
+ HeadersInit headers;
+ BodyInit? body;
+ USVString referrer;
+ ReferrerPolicy referrerPolicy;
+ RequestMode mode;
+ RequestCredentials credentials;
+ RequestCache cache;
+ RequestRedirect redirect;
+ DOMString integrity;
+ boolean keepalive;
+ AbortSignal? signal;
+ RequestDuplex duplex;
+ RequestPriority priority;
+ any window; // can only be set to null
+};
+
+enum RequestDestination { "", "audio", "audioworklet", "document", "embed", "font", "frame", "iframe", "image", "json", "manifest", "object", "paintworklet", "report", "script", "sharedworker", "style", "track", "video", "worker", "xslt" };
+enum RequestMode { "navigate", "same-origin", "no-cors", "cors" };
+enum RequestCredentials { "omit", "same-origin", "include" };
+enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" };
+enum RequestRedirect { "follow", "error", "manual" };
+enum RequestDuplex { "half" };
+enum RequestPriority { "high", "low", "auto" };
+
+[Exposed=(Window,Worker)]
+interface Response {
+ constructor(optional BodyInit? body = null, optional ResponseInit init = {});
+
+ [NewObject] static Response error();
+ [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);
+ [NewObject] static Response json(any data, optional ResponseInit init = {});
+
+ readonly attribute ResponseType type;
+
+ readonly attribute USVString url;
+ readonly attribute boolean redirected;
+ readonly attribute unsigned short status;
+ readonly attribute boolean ok;
+ readonly attribute ByteString statusText;
+ [SameObject] readonly attribute Headers headers;
+
+ [NewObject] Response clone();
+};
+Response includes Body;
+
+dictionary ResponseInit {
+ unsigned short status = 200;
+ ByteString statusText = "";
+ HeadersInit headers;
+};
+
+enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" };
+
+partial interface mixin WindowOrWorkerGlobalScope {
+ [NewObject] Promise fetch(RequestInfo input, optional RequestInit init = {});
+};
+
+dictionary DeferredRequestInit : RequestInit {
+ DOMHighResTimeStamp activateAfter;
+};
+
+[Exposed=Window]
+interface FetchLaterResult {
+ readonly attribute boolean activated;
+};
+
+partial interface Window {
+ [NewObject, SecureContext] FetchLaterResult fetchLater(RequestInfo input, optional DeferredRequestInit init = {});
+};
diff --git a/test/fixtures/wpt/interfaces/html.idl b/test/fixtures/wpt/interfaces/html.idl
index 9c84e6a67efa4f..dabe06beb2a878 100644
--- a/test/fixtures/wpt/interfaces/html.idl
+++ b/test/fixtures/wpt/interfaces/html.idl
@@ -110,21 +110,21 @@ interface HTMLElement : Element {
[HTMLConstructor] constructor();
// metadata attributes
- [CEReactions] attribute DOMString title;
- [CEReactions] attribute DOMString lang;
+ [CEReactions, Reflect] attribute DOMString title;
+ [CEReactions, Reflect] attribute DOMString lang;
[CEReactions] attribute boolean translate;
[CEReactions] attribute DOMString dir;
// user interaction
[CEReactions] attribute (boolean or unrestricted double or DOMString)? hidden;
- [CEReactions] attribute boolean inert;
+ [CEReactions, Reflect] attribute boolean inert;
undefined click();
- [CEReactions] attribute DOMString accessKey;
+ [CEReactions, Reflect] attribute DOMString accessKey;
readonly attribute DOMString accessKeyLabel;
[CEReactions] attribute boolean draggable;
[CEReactions] attribute boolean spellcheck;
- [CEReactions] attribute DOMString writingSuggestions;
- [CEReactions] attribute DOMString autocapitalize;
+ [CEReactions, ReflectSetter] attribute DOMString writingSuggestions;
+ [CEReactions, ReflectSetter] attribute DOMString autocapitalize;
[CEReactions] attribute boolean autocorrect;
[CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText;
@@ -137,6 +137,9 @@ interface HTMLElement : Element {
undefined hidePopover();
boolean togglePopover(optional (TogglePopoverOptions or boolean) options = {});
[CEReactions] attribute DOMString? popover;
+
+ [CEReactions, Reflect, ReflectRange=(0, 8)] attribute unsigned long headingOffset;
+ [CEReactions, Reflect] attribute boolean headingReset;
};
dictionary ShowPopoverOptions {
@@ -160,8 +163,8 @@ interface mixin HTMLOrSVGElement {
[SameObject] readonly attribute DOMStringMap dataset;
attribute DOMString nonce; // intentionally no [CEReactions]
- [CEReactions] attribute boolean autofocus;
- [CEReactions] attribute long tabIndex;
+ [CEReactions, Reflect] attribute boolean autofocus;
+ [CEReactions, ReflectSetter] attribute long tabIndex;
undefined focus(optional FocusOptions options = {});
undefined blur();
};
@@ -197,29 +200,29 @@ interface HTMLTitleElement : HTMLElement {
interface HTMLBaseElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString href;
- [CEReactions] attribute DOMString target;
+ [CEReactions, ReflectSetter] attribute USVString href;
+ [CEReactions, Reflect] attribute DOMString target;
};
[Exposed=Window]
interface HTMLLinkElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString href;
+ [CEReactions, ReflectURL] attribute USVString href;
[CEReactions] attribute DOMString? crossOrigin;
- [CEReactions] attribute DOMString rel;
+ [CEReactions, Reflect] attribute DOMString rel;
[CEReactions] attribute DOMString as;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
- [CEReactions] attribute DOMString media;
- [CEReactions] attribute DOMString integrity;
- [CEReactions] attribute DOMString hreflang;
- [CEReactions] attribute DOMString type;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList sizes;
- [CEReactions] attribute USVString imageSrcset;
- [CEReactions] attribute DOMString imageSizes;
+ [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList;
+ [CEReactions, Reflect] attribute DOMString media;
+ [CEReactions, Reflect] attribute DOMString integrity;
+ [CEReactions, Reflect] attribute DOMString hreflang;
+ [CEReactions, Reflect] attribute DOMString type;
+ [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList sizes;
+ [CEReactions, Reflect] attribute USVString imageSrcset;
+ [CEReactions, Reflect] attribute DOMString imageSizes;
[CEReactions] attribute DOMString referrerPolicy;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking;
- [CEReactions] attribute boolean disabled;
+ [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList blocking;
+ [CEReactions, Reflect] attribute boolean disabled;
[CEReactions] attribute DOMString fetchPriority;
// also has obsolete members
@@ -230,10 +233,10 @@ HTMLLinkElement includes LinkStyle;
interface HTMLMetaElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString httpEquiv;
- [CEReactions] attribute DOMString content;
- [CEReactions] attribute DOMString media;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect="http-equiv"] attribute DOMString httpEquiv;
+ [CEReactions, Reflect] attribute DOMString content;
+ [CEReactions, Reflect] attribute DOMString media;
// also has obsolete members
};
@@ -243,8 +246,8 @@ interface HTMLStyleElement : HTMLElement {
[HTMLConstructor] constructor();
attribute boolean disabled;
- [CEReactions] attribute DOMString media;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking;
+ [CEReactions, Reflect] attribute DOMString media;
+ [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList blocking;
// also has obsolete members
};
@@ -291,16 +294,16 @@ interface HTMLPreElement : HTMLElement {
interface HTMLQuoteElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString cite;
+ [CEReactions, ReflectURL] attribute USVString cite;
};
[Exposed=Window]
interface HTMLOListElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute boolean reversed;
- [CEReactions] attribute long start;
- [CEReactions] attribute DOMString type;
+ [CEReactions, Reflect] attribute boolean reversed;
+ [CEReactions, Reflect, ReflectDefault=1] attribute long start;
+ [CEReactions, Reflect] attribute DOMString type;
// also has obsolete members
};
@@ -323,7 +326,7 @@ interface HTMLMenuElement : HTMLElement {
interface HTMLLIElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute long value;
+ [CEReactions, Reflect] attribute long value;
// also has obsolete members
};
@@ -346,13 +349,13 @@ interface HTMLDivElement : HTMLElement {
interface HTMLAnchorElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString target;
- [CEReactions] attribute DOMString download;
- [CEReactions] attribute USVString ping;
- [CEReactions] attribute DOMString rel;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
- [CEReactions] attribute DOMString hreflang;
- [CEReactions] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString target;
+ [CEReactions, Reflect] attribute DOMString download;
+ [CEReactions, Reflect] attribute USVString ping;
+ [CEReactions, Reflect] attribute DOMString rel;
+ [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList;
+ [CEReactions, Reflect] attribute DOMString hreflang;
+ [CEReactions, Reflect] attribute DOMString type;
[CEReactions] attribute DOMString text;
@@ -366,14 +369,14 @@ HTMLAnchorElement includes HTMLHyperlinkElementUtils;
interface HTMLDataElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString value;
+ [CEReactions, Reflect] attribute DOMString value;
};
[Exposed=Window]
interface HTMLTimeElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString dateTime;
+ [CEReactions, Reflect] attribute DOMString dateTime;
};
[Exposed=Window]
@@ -389,7 +392,7 @@ interface HTMLBRElement : HTMLElement {
};
interface mixin HTMLHyperlinkElementUtils {
- [CEReactions] stringifier attribute USVString href;
+ [CEReactions, ReflectSetter] stringifier attribute USVString href;
readonly attribute USVString origin;
[CEReactions] attribute USVString protocol;
[CEReactions] attribute USVString username;
@@ -406,8 +409,8 @@ interface mixin HTMLHyperlinkElementUtils {
interface HTMLModElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString cite;
- [CEReactions] attribute DOMString dateTime;
+ [CEReactions, ReflectURL] attribute USVString cite;
+ [CEReactions, Reflect] attribute DOMString dateTime;
};
[Exposed=Window]
@@ -419,13 +422,13 @@ interface HTMLPictureElement : HTMLElement {
interface HTMLSourceElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString src;
- [CEReactions] attribute DOMString type;
- [CEReactions] attribute USVString srcset;
- [CEReactions] attribute DOMString sizes;
- [CEReactions] attribute DOMString media;
- [CEReactions] attribute unsigned long width;
- [CEReactions] attribute unsigned long height;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute DOMString type;
+ [CEReactions, Reflect] attribute USVString srcset;
+ [CEReactions, Reflect] attribute DOMString sizes;
+ [CEReactions, Reflect] attribute DOMString media;
+ [CEReactions, Reflect] attribute unsigned long width;
+ [CEReactions, Reflect] attribute unsigned long height;
};
[Exposed=Window,
@@ -433,15 +436,15 @@ interface HTMLSourceElement : HTMLElement {
interface HTMLImageElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString alt;
- [CEReactions] attribute USVString src;
- [CEReactions] attribute USVString srcset;
- [CEReactions] attribute DOMString sizes;
+ [CEReactions, Reflect] attribute DOMString alt;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute USVString srcset;
+ [CEReactions, Reflect] attribute DOMString sizes;
[CEReactions] attribute DOMString? crossOrigin;
- [CEReactions] attribute DOMString useMap;
- [CEReactions] attribute boolean isMap;
- [CEReactions] attribute unsigned long width;
- [CEReactions] attribute unsigned long height;
+ [CEReactions, Reflect] attribute DOMString useMap;
+ [CEReactions, Reflect] attribute boolean isMap;
+ [CEReactions, ReflectSetter] attribute unsigned long width;
+ [CEReactions, ReflectSetter] attribute unsigned long height;
readonly attribute unsigned long naturalWidth;
readonly attribute unsigned long naturalHeight;
readonly attribute boolean complete;
@@ -460,14 +463,14 @@ interface HTMLImageElement : HTMLElement {
interface HTMLIFrameElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString src;
+ [CEReactions, ReflectURL] attribute USVString src;
[CEReactions] attribute (TrustedHTML or DOMString) srcdoc;
- [CEReactions] attribute DOMString name;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList sandbox;
- [CEReactions] attribute DOMString allow;
- [CEReactions] attribute boolean allowFullscreen;
- [CEReactions] attribute DOMString width;
- [CEReactions] attribute DOMString height;
+ [CEReactions, Reflect] attribute DOMString name;
+ [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList sandbox;
+ [CEReactions, Reflect] attribute DOMString allow;
+ [CEReactions, Reflect] attribute boolean allowFullscreen;
+ [CEReactions, Reflect] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString height;
[CEReactions] attribute DOMString referrerPolicy;
[CEReactions] attribute DOMString loading;
readonly attribute Document? contentDocument;
@@ -481,10 +484,10 @@ interface HTMLIFrameElement : HTMLElement {
interface HTMLEmbedElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString src;
- [CEReactions] attribute DOMString type;
- [CEReactions] attribute DOMString width;
- [CEReactions] attribute DOMString height;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString height;
Document? getSVGDocument();
// also has obsolete members
@@ -494,12 +497,12 @@ interface HTMLEmbedElement : HTMLElement {
interface HTMLObjectElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString data;
- [CEReactions] attribute DOMString type;
- [CEReactions] attribute DOMString name;
+ [CEReactions, ReflectURL] attribute USVString data;
+ [CEReactions, Reflect] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString name;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute DOMString width;
- [CEReactions] attribute DOMString height;
+ [CEReactions, Reflect] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString height;
readonly attribute Document? contentDocument;
readonly attribute WindowProxy? contentWindow;
Document? getSVGDocument();
@@ -518,12 +521,12 @@ interface HTMLObjectElement : HTMLElement {
interface HTMLVideoElement : HTMLMediaElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute unsigned long width;
- [CEReactions] attribute unsigned long height;
+ [CEReactions, Reflect] attribute unsigned long width;
+ [CEReactions, Reflect] attribute unsigned long height;
readonly attribute unsigned long videoWidth;
readonly attribute unsigned long videoHeight;
- [CEReactions] attribute USVString poster;
- [CEReactions] attribute boolean playsInline;
+ [CEReactions, ReflectURL] attribute USVString poster;
+ [CEReactions, Reflect] attribute boolean playsInline;
};
[Exposed=Window,
@@ -537,10 +540,10 @@ interface HTMLTrackElement : HTMLElement {
[HTMLConstructor] constructor();
[CEReactions] attribute DOMString kind;
- [CEReactions] attribute USVString src;
- [CEReactions] attribute DOMString srclang;
- [CEReactions] attribute DOMString label;
- [CEReactions] attribute boolean default;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute DOMString srclang;
+ [CEReactions, Reflect] attribute DOMString label;
+ [CEReactions, Reflect] attribute boolean default;
const unsigned short NONE = 0;
const unsigned short LOADING = 1;
@@ -561,7 +564,7 @@ interface HTMLMediaElement : HTMLElement {
readonly attribute MediaError? error;
// network state
- [CEReactions] attribute USVString src;
+ [CEReactions, ReflectURL] attribute USVString src;
attribute MediaProvider? srcObject;
readonly attribute USVString currentSrc;
[CEReactions] attribute DOMString? crossOrigin;
@@ -596,16 +599,16 @@ interface HTMLMediaElement : HTMLElement {
readonly attribute TimeRanges played;
readonly attribute TimeRanges seekable;
readonly attribute boolean ended;
- [CEReactions] attribute boolean autoplay;
- [CEReactions] attribute boolean loop;
+ [CEReactions, Reflect] attribute boolean autoplay;
+ [CEReactions, Reflect] attribute boolean loop;
Promise play();
undefined pause();
// controls
- [CEReactions] attribute boolean controls;
+ [CEReactions, Reflect] attribute boolean controls;
attribute double volume;
attribute boolean muted;
- [CEReactions] attribute boolean defaultMuted;
+ [CEReactions, Reflect="muted"] attribute boolean defaultMuted;
// tracks
[SameObject] readonly attribute AudioTrackList audioTracks;
@@ -742,7 +745,7 @@ dictionary TrackEventInit : EventInit {
interface HTMLMapElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString name;
[SameObject] readonly attribute HTMLCollection areas;
};
@@ -750,14 +753,14 @@ interface HTMLMapElement : HTMLElement {
interface HTMLAreaElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString alt;
- [CEReactions] attribute DOMString coords;
- [CEReactions] attribute DOMString shape;
- [CEReactions] attribute DOMString target;
- [CEReactions] attribute DOMString download;
- [CEReactions] attribute USVString ping;
- [CEReactions] attribute DOMString rel;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+ [CEReactions, Reflect] attribute DOMString alt;
+ [CEReactions, Reflect] attribute DOMString coords;
+ [CEReactions, Reflect] attribute DOMString shape;
+ [CEReactions, Reflect] attribute DOMString target;
+ [CEReactions, Reflect] attribute DOMString download;
+ [CEReactions, Reflect] attribute USVString ping;
+ [CEReactions, Reflect] attribute DOMString rel;
+ [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList;
[CEReactions] attribute DOMString referrerPolicy;
// also has obsolete members
@@ -801,7 +804,7 @@ interface HTMLTableCaptionElement : HTMLElement {
interface HTMLTableColElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute unsigned long span;
+ [CEReactions, Reflect, ReflectDefault=1, ReflectRange=(1, 1000)] attribute unsigned long span;
// also has obsolete members
};
@@ -834,13 +837,13 @@ interface HTMLTableRowElement : HTMLElement {
interface HTMLTableCellElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute unsigned long colSpan;
- [CEReactions] attribute unsigned long rowSpan;
- [CEReactions] attribute DOMString headers;
+ [CEReactions, Reflect, ReflectDefault=1, ReflectRange=(1, 1000)] attribute unsigned long colSpan;
+ [CEReactions, Reflect, ReflectDefault=1, ReflectRange=(0, 65534)] attribute unsigned long rowSpan;
+ [CEReactions, Reflect] attribute DOMString headers;
readonly attribute long cellIndex;
[CEReactions] attribute DOMString scope; // only conforming for th elements
- [CEReactions] attribute DOMString abbr; // only conforming for th elements
+ [CEReactions, Reflect] attribute DOMString abbr; // only conforming for th elements
// also has obsolete members
};
@@ -851,17 +854,17 @@ interface HTMLTableCellElement : HTMLElement {
interface HTMLFormElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString acceptCharset;
- [CEReactions] attribute USVString action;
+ [CEReactions, Reflect="accept-charset"] attribute DOMString acceptCharset;
+ [CEReactions, ReflectSetter] attribute USVString action;
[CEReactions] attribute DOMString autocomplete;
[CEReactions] attribute DOMString enctype;
[CEReactions] attribute DOMString encoding;
[CEReactions] attribute DOMString method;
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute boolean noValidate;
- [CEReactions] attribute DOMString target;
- [CEReactions] attribute DOMString rel;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute boolean noValidate;
+ [CEReactions, Reflect] attribute DOMString target;
+ [CEReactions, Reflect] attribute DOMString rel;
+ [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList;
[SameObject] readonly attribute HTMLFormControlsCollection elements;
readonly attribute unsigned long length;
@@ -880,7 +883,7 @@ interface HTMLLabelElement : HTMLElement {
[HTMLConstructor] constructor();
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute DOMString htmlFor;
+ [CEReactions, Reflect="for"] attribute DOMString htmlFor;
readonly attribute HTMLElement? control;
};
@@ -888,44 +891,44 @@ interface HTMLLabelElement : HTMLElement {
interface HTMLInputElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString accept;
- [CEReactions] attribute boolean alpha;
- [CEReactions] attribute DOMString alt;
- [CEReactions] attribute DOMString autocomplete;
- [CEReactions] attribute boolean defaultChecked;
+ [CEReactions, Reflect] attribute DOMString accept;
+ [CEReactions, Reflect] attribute boolean alpha;
+ [CEReactions, Reflect] attribute DOMString alt;
+ [CEReactions, ReflectSetter] attribute DOMString autocomplete;
+ [CEReactions, Reflect="checked"] attribute boolean defaultChecked;
attribute boolean checked;
[CEReactions] attribute DOMString colorSpace;
- [CEReactions] attribute DOMString dirName;
- [CEReactions] attribute boolean disabled;
+ [CEReactions, Reflect] attribute DOMString dirName;
+ [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form;
attribute FileList? files;
- [CEReactions] attribute USVString formAction;
+ [CEReactions, ReflectSetter] attribute USVString formAction;
[CEReactions] attribute DOMString formEnctype;
[CEReactions] attribute DOMString formMethod;
- [CEReactions] attribute boolean formNoValidate;
- [CEReactions] attribute DOMString formTarget;
- [CEReactions] attribute unsigned long height;
+ [CEReactions, Reflect] attribute boolean formNoValidate;
+ [CEReactions, Reflect] attribute DOMString formTarget;
+ [CEReactions, ReflectSetter] attribute unsigned long height;
attribute boolean indeterminate;
readonly attribute HTMLDataListElement? list;
- [CEReactions] attribute DOMString max;
- [CEReactions] attribute long maxLength;
- [CEReactions] attribute DOMString min;
- [CEReactions] attribute long minLength;
- [CEReactions] attribute boolean multiple;
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString pattern;
- [CEReactions] attribute DOMString placeholder;
- [CEReactions] attribute boolean readOnly;
- [CEReactions] attribute boolean required;
- [CEReactions] attribute unsigned long size;
- [CEReactions] attribute USVString src;
- [CEReactions] attribute DOMString step;
+ [CEReactions, Reflect] attribute DOMString max;
+ [CEReactions, ReflectNonNegative] attribute long maxLength;
+ [CEReactions, Reflect] attribute DOMString min;
+ [CEReactions, ReflectNonNegative] attribute long minLength;
+ [CEReactions, Reflect] attribute boolean multiple;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString pattern;
+ [CEReactions, Reflect] attribute DOMString placeholder;
+ [CEReactions, Reflect] attribute boolean readOnly;
+ [CEReactions, Reflect] attribute boolean required;
+ [CEReactions, Reflect] attribute unsigned long size;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute DOMString step;
[CEReactions] attribute DOMString type;
- [CEReactions] attribute DOMString defaultValue;
+ [CEReactions, Reflect="value"] attribute DOMString defaultValue;
[CEReactions] attribute [LegacyNullToEmptyString] DOMString value;
attribute object? valueAsDate;
attribute unrestricted double valueAsNumber;
- [CEReactions] attribute unsigned long width;
+ [CEReactions, ReflectSetter] attribute unsigned long width;
undefined stepUp(optional long n = 1);
undefined stepDown(optional long n = 1);
@@ -951,24 +954,24 @@ interface HTMLInputElement : HTMLElement {
// also has obsolete members
};
-HTMLInputElement includes PopoverInvokerElement;
+HTMLInputElement includes PopoverTargetAttributes;
[Exposed=Window]
interface HTMLButtonElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString command;
- [CEReactions] attribute Element? commandForElement;
- [CEReactions] attribute boolean disabled;
+ [CEReactions, ReflectSetter] attribute DOMString command;
+ [CEReactions, Reflect] attribute Element? commandForElement;
+ [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute USVString formAction;
+ [CEReactions, ReflectSetter] attribute USVString formAction;
[CEReactions] attribute DOMString formEnctype;
[CEReactions] attribute DOMString formMethod;
- [CEReactions] attribute boolean formNoValidate;
- [CEReactions] attribute DOMString formTarget;
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString type;
- [CEReactions] attribute DOMString value;
+ [CEReactions, Reflect] attribute boolean formNoValidate;
+ [CEReactions, Reflect] attribute DOMString formTarget;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, ReflectSetter] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString value;
readonly attribute boolean willValidate;
readonly attribute ValidityState validity;
@@ -979,19 +982,19 @@ interface HTMLButtonElement : HTMLElement {
readonly attribute NodeList labels;
};
-HTMLButtonElement includes PopoverInvokerElement;
+HTMLButtonElement includes PopoverTargetAttributes;
[Exposed=Window]
interface HTMLSelectElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString autocomplete;
- [CEReactions] attribute boolean disabled;
+ [CEReactions, ReflectSetter] attribute DOMString autocomplete;
+ [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute boolean multiple;
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute boolean required;
- [CEReactions] attribute unsigned long size;
+ [CEReactions, Reflect] attribute boolean multiple;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute boolean required;
+ [CEReactions, Reflect, ReflectDefault=0] attribute unsigned long size;
readonly attribute DOMString type;
@@ -1031,8 +1034,8 @@ interface HTMLDataListElement : HTMLElement {
interface HTMLOptGroupElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute boolean disabled;
- [CEReactions] attribute DOMString label;
+ [CEReactions, Reflect] attribute boolean disabled;
+ [CEReactions, Reflect] attribute DOMString label;
};
[Exposed=Window,
@@ -1040,12 +1043,12 @@ interface HTMLOptGroupElement : HTMLElement {
interface HTMLOptionElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute boolean disabled;
+ [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute DOMString label;
- [CEReactions] attribute boolean defaultSelected;
+ [CEReactions, ReflectSetter] attribute DOMString label;
+ [CEReactions, Reflect="selected"] attribute boolean defaultSelected;
attribute boolean selected;
- [CEReactions] attribute DOMString value;
+ [CEReactions, ReflectSetter] attribute DOMString value;
[CEReactions] attribute DOMString text;
readonly attribute long index;
@@ -1055,19 +1058,19 @@ interface HTMLOptionElement : HTMLElement {
interface HTMLTextAreaElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString autocomplete;
- [CEReactions] attribute unsigned long cols;
- [CEReactions] attribute DOMString dirName;
- [CEReactions] attribute boolean disabled;
+ [CEReactions, ReflectSetter] attribute DOMString autocomplete;
+ [CEReactions, ReflectPositiveWithFallback, ReflectDefault=20] attribute unsigned long cols;
+ [CEReactions, Reflect] attribute DOMString dirName;
+ [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute long maxLength;
- [CEReactions] attribute long minLength;
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString placeholder;
- [CEReactions] attribute boolean readOnly;
- [CEReactions] attribute boolean required;
- [CEReactions] attribute unsigned long rows;
- [CEReactions] attribute DOMString wrap;
+ [CEReactions, ReflectNonNegative] attribute long maxLength;
+ [CEReactions, ReflectNonNegative] attribute long minLength;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString placeholder;
+ [CEReactions, Reflect] attribute boolean readOnly;
+ [CEReactions, Reflect] attribute boolean required;
+ [CEReactions, ReflectPositiveWithFallback, ReflectDefault=2] attribute unsigned long rows;
+ [CEReactions, Reflect] attribute DOMString wrap;
readonly attribute DOMString type;
[CEReactions] attribute DOMString defaultValue;
@@ -1096,9 +1099,9 @@ interface HTMLTextAreaElement : HTMLElement {
interface HTMLOutputElement : HTMLElement {
[HTMLConstructor] constructor();
- [SameObject, PutForwards=value] readonly attribute DOMTokenList htmlFor;
+ [SameObject, PutForwards=value, Reflect="for"] readonly attribute DOMTokenList htmlFor;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString name;
readonly attribute DOMString type;
[CEReactions] attribute DOMString defaultValue;
@@ -1118,8 +1121,8 @@ interface HTMLOutputElement : HTMLElement {
interface HTMLProgressElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute double value;
- [CEReactions] attribute double max;
+ [CEReactions, ReflectSetter] attribute double value;
+ [CEReactions, ReflectPositive, ReflectDefault=1.0] attribute double max;
readonly attribute double position;
readonly attribute NodeList labels;
};
@@ -1128,12 +1131,12 @@ interface HTMLProgressElement : HTMLElement {
interface HTMLMeterElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute double value;
- [CEReactions] attribute double min;
- [CEReactions] attribute double max;
- [CEReactions] attribute double low;
- [CEReactions] attribute double high;
- [CEReactions] attribute double optimum;
+ [CEReactions, ReflectSetter] attribute double value;
+ [CEReactions, ReflectSetter] attribute double min;
+ [CEReactions, ReflectSetter] attribute double max;
+ [CEReactions, ReflectSetter] attribute double low;
+ [CEReactions, ReflectSetter] attribute double high;
+ [CEReactions, ReflectSetter] attribute double optimum;
readonly attribute NodeList labels;
};
@@ -1141,9 +1144,9 @@ interface HTMLMeterElement : HTMLElement {
interface HTMLFieldSetElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute boolean disabled;
+ [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form;
- [CEReactions] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString name;
readonly attribute DOMString type;
@@ -1166,6 +1169,11 @@ interface HTMLLegendElement : HTMLElement {
// also has obsolete members
};
+[Exposed=Window]
+interface HTMLSelectedContentElement : HTMLElement {
+ [HTMLConstructor] constructor();
+};
+
enum SelectionMode {
"select",
"start",
@@ -1214,17 +1222,17 @@ dictionary FormDataEventInit : EventInit {
interface HTMLDetailsElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute boolean open;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute boolean open;
};
[Exposed=Window]
interface HTMLDialogElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute boolean open;
+ [CEReactions, Reflect] attribute boolean open;
attribute DOMString returnValue;
- [CEReactions] attribute DOMString closedBy;
+ [CEReactions, ReflectSetter] attribute DOMString closedBy;
[CEReactions] undefined show();
[CEReactions] undefined showModal();
[CEReactions] undefined close(optional DOMString returnValue);
@@ -1235,18 +1243,19 @@ interface HTMLDialogElement : HTMLElement {
interface HTMLScriptElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute USVString src;
- [CEReactions] attribute DOMString type;
- [CEReactions] attribute boolean noModule;
+ [CEReactions, Reflect] attribute DOMString type;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute boolean noModule;
[CEReactions] attribute boolean async;
- [CEReactions] attribute boolean defer;
+ [CEReactions, Reflect] attribute boolean defer;
+ [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList blocking;
[CEReactions] attribute DOMString? crossOrigin;
- [CEReactions] attribute DOMString text;
- [CEReactions] attribute DOMString integrity;
[CEReactions] attribute DOMString referrerPolicy;
- [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking;
+ [CEReactions, Reflect] attribute DOMString integrity;
[CEReactions] attribute DOMString fetchPriority;
+ [CEReactions] attribute DOMString text;
+
static boolean supports(DOMString type);
// also has obsolete members
@@ -1258,17 +1267,17 @@ interface HTMLTemplateElement : HTMLElement {
readonly attribute DocumentFragment content;
[CEReactions] attribute DOMString shadowRootMode;
- [CEReactions] attribute boolean shadowRootDelegatesFocus;
- [CEReactions] attribute boolean shadowRootClonable;
- [CEReactions] attribute boolean shadowRootSerializable;
- [CEReactions] attribute DOMString shadowRootCustomElementRegistry;
+ [CEReactions, Reflect] attribute boolean shadowRootDelegatesFocus;
+ [CEReactions, Reflect] attribute boolean shadowRootClonable;
+ [CEReactions, Reflect] attribute boolean shadowRootSerializable;
+ [CEReactions, Reflect] attribute DOMString shadowRootCustomElementRegistry;
};
[Exposed=Window]
interface HTMLSlotElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString name;
sequence assignedNodes(optional AssignedNodesOptions options = {});
sequence assignedElements(optional AssignedNodesOptions options = {});
undefined assign((Element or Text)... nodes);
@@ -1306,8 +1315,6 @@ typedef (HTMLOrSVGImageElement or
OffscreenCanvas or
VideoFrame) CanvasImageSource;
-enum PredefinedColorSpace { "srgb", "display-p3" };
-
enum CanvasColorType { "unorm8", "float16" };
enum CanvasFillRule { "nonzero", "evenodd" };
@@ -1606,6 +1613,8 @@ OffscreenCanvasRenderingContext2D includes CanvasPathDrawingStyles;
OffscreenCanvasRenderingContext2D includes CanvasTextDrawingStyles;
OffscreenCanvasRenderingContext2D includes CanvasPath;
+enum PredefinedColorSpace { "srgb", "srgb-linear", "display-p3", "display-p3-linear" };
+
[Exposed=Window]
interface CustomElementRegistry {
constructor();
@@ -1615,7 +1624,7 @@ interface CustomElementRegistry {
DOMString? getName(CustomElementConstructor constructor);
Promise whenDefined(DOMString name);
[CEReactions] undefined upgrade(Node root);
- undefined initialize(Node root);
+ [CEReactions] undefined initialize(Node root);
};
callback CustomElementConstructor = HTMLElement ();
@@ -1694,11 +1703,13 @@ interface ToggleEvent : Event {
constructor(DOMString type, optional ToggleEventInit eventInitDict = {});
readonly attribute DOMString oldState;
readonly attribute DOMString newState;
+ readonly attribute Element? source;
};
dictionary ToggleEventInit : EventInit {
DOMString oldState = "";
DOMString newState = "";
+ Element? source = null;
};
[Exposed=Window]
@@ -1791,11 +1802,23 @@ dictionary DragEventInit : MouseEventInit {
DataTransfer? dataTransfer = null;
};
-interface mixin PopoverInvokerElement {
- [CEReactions] attribute Element? popoverTargetElement;
+interface mixin PopoverTargetAttributes {
+ [CEReactions, Reflect] attribute Element? popoverTargetElement;
[CEReactions] attribute DOMString popoverTargetAction;
};
+[Exposed=*]
+interface Origin {
+ constructor();
+
+ static Origin from(any value);
+
+ readonly attribute boolean opaque;
+
+ boolean isSameOrigin(Origin other);
+ boolean isSameSite(Origin other);
+};
+
[Global=Window,
Exposed=Window,
LegacyUnenumerableNamedProperties]
@@ -1807,7 +1830,7 @@ interface Window : EventTarget {
attribute DOMString name;
[PutForwards=href, LegacyUnforgeable] readonly attribute Location location;
readonly attribute History history;
- readonly attribute Navigation navigation;
+ [Replaceable] readonly attribute Navigation navigation;
readonly attribute CustomElementRegistry customElements;
[Replaceable] readonly attribute BarProp locationbar;
[Replaceable] readonly attribute BarProp menubar;
@@ -1974,6 +1997,8 @@ interface NavigationHistoryEntry : EventTarget {
interface NavigationTransition {
readonly attribute NavigationType navigationType;
readonly attribute NavigationHistoryEntry from;
+ readonly attribute NavigationDestination to;
+ readonly attribute Promise committed;
readonly attribute Promise finished;
};
@@ -2019,6 +2044,7 @@ dictionary NavigateEventInit : EventInit {
};
dictionary NavigationInterceptOptions {
+ NavigationPrecommitHandler precommitHandler;
NavigationInterceptHandler handler;
NavigationFocusReset focusReset;
NavigationScrollBehavior scroll;
@@ -2036,6 +2062,14 @@ enum NavigationScrollBehavior {
callback NavigationInterceptHandler = Promise ();
+[Exposed=Window]
+interface NavigationPrecommitController {
+ undefined redirect(USVString url, optional NavigationNavigateOptions options = {});
+ undefined addHandler(NavigationInterceptHandler handler);
+};
+
+callback NavigationPrecommitHandler = Promise (NavigationPrecommitController controller);
+
[Exposed=Window]
interface NavigationDestination {
readonly attribute USVString url;
@@ -2649,9 +2683,9 @@ interface Worker : EventTarget {
};
dictionary WorkerOptions {
+ DOMString name = "";
WorkerType type = "classic";
RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module"
- DOMString name = "";
};
enum WorkerType { "classic", "module" };
@@ -2748,17 +2782,17 @@ dictionary StorageEventInit : EventInit {
interface HTMLMarqueeElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString behavior;
- [CEReactions] attribute DOMString bgColor;
- [CEReactions] attribute DOMString direction;
- [CEReactions] attribute DOMString height;
- [CEReactions] attribute unsigned long hspace;
+ [CEReactions, Reflect] attribute DOMString behavior;
+ [CEReactions, Reflect] attribute DOMString bgColor;
+ [CEReactions, Reflect] attribute DOMString direction;
+ [CEReactions, Reflect] attribute DOMString height;
+ [CEReactions, Reflect] attribute unsigned long hspace;
[CEReactions] attribute long loop;
- [CEReactions] attribute unsigned long scrollAmount;
- [CEReactions] attribute unsigned long scrollDelay;
- [CEReactions] attribute boolean trueSpeed;
- [CEReactions] attribute unsigned long vspace;
- [CEReactions] attribute DOMString width;
+ [CEReactions, Reflect, ReflectDefault=6] attribute unsigned long scrollAmount;
+ [CEReactions, Reflect, ReflectDefault=85] attribute unsigned long scrollDelay;
+ [CEReactions, Reflect] attribute boolean trueSpeed;
+ [CEReactions, Reflect] attribute unsigned long vspace;
+ [CEReactions, Reflect] attribute DOMString width;
undefined start();
undefined stop();
@@ -2768,8 +2802,8 @@ interface HTMLMarqueeElement : HTMLElement {
interface HTMLFrameSetElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString cols;
- [CEReactions] attribute DOMString rows;
+ [CEReactions, Reflect] attribute DOMString cols;
+ [CEReactions, Reflect] attribute DOMString rows;
};
HTMLFrameSetElement includes WindowEventHandlers;
@@ -2777,242 +2811,242 @@ HTMLFrameSetElement includes WindowEventHandlers;
interface HTMLFrameElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString scrolling;
- [CEReactions] attribute USVString src;
- [CEReactions] attribute DOMString frameBorder;
- [CEReactions] attribute USVString longDesc;
- [CEReactions] attribute boolean noResize;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString scrolling;
+ [CEReactions, ReflectURL] attribute USVString src;
+ [CEReactions, Reflect] attribute DOMString frameBorder;
+ [CEReactions, ReflectURL] attribute USVString longDesc;
+ [CEReactions, Reflect] attribute boolean noResize;
readonly attribute Document? contentDocument;
readonly attribute WindowProxy? contentWindow;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginHeight;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginWidth;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginHeight;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginWidth;
};
partial interface HTMLAnchorElement {
- [CEReactions] attribute DOMString coords;
- [CEReactions] attribute DOMString charset;
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString rev;
- [CEReactions] attribute DOMString shape;
+ [CEReactions, Reflect] attribute DOMString coords;
+ [CEReactions, Reflect] attribute DOMString charset;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString rev;
+ [CEReactions, Reflect] attribute DOMString shape;
};
partial interface HTMLAreaElement {
- [CEReactions] attribute boolean noHref;
+ [CEReactions, Reflect] attribute boolean noHref;
};
partial interface HTMLBodyElement {
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString text;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString link;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString vLink;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString aLink;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
- [CEReactions] attribute DOMString background;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString text;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString link;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString vLink;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString aLink;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor;
+ [CEReactions, Reflect] attribute DOMString background;
};
partial interface HTMLBRElement {
- [CEReactions] attribute DOMString clear;
+ [CEReactions, Reflect] attribute DOMString clear;
};
partial interface HTMLTableCaptionElement {
- [CEReactions] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString align;
};
partial interface HTMLTableColElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString ch;
- [CEReactions] attribute DOMString chOff;
- [CEReactions] attribute DOMString vAlign;
- [CEReactions] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect="char"] attribute DOMString ch;
+ [CEReactions, Reflect="charoff"] attribute DOMString chOff;
+ [CEReactions, Reflect] attribute DOMString vAlign;
+ [CEReactions, Reflect] attribute DOMString width;
};
[Exposed=Window]
interface HTMLDirectoryElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute boolean compact;
+ [CEReactions, Reflect] attribute boolean compact;
};
partial interface HTMLDivElement {
- [CEReactions] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString align;
};
partial interface HTMLDListElement {
- [CEReactions] attribute boolean compact;
+ [CEReactions, Reflect] attribute boolean compact;
};
partial interface HTMLEmbedElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString name;
};
[Exposed=Window]
interface HTMLFontElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString color;
- [CEReactions] attribute DOMString face;
- [CEReactions] attribute DOMString size;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString color;
+ [CEReactions, Reflect] attribute DOMString face;
+ [CEReactions, Reflect] attribute DOMString size;
};
partial interface HTMLHeadingElement {
- [CEReactions] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString align;
};
partial interface HTMLHRElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString color;
- [CEReactions] attribute boolean noShade;
- [CEReactions] attribute DOMString size;
- [CEReactions] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString color;
+ [CEReactions, Reflect] attribute boolean noShade;
+ [CEReactions, Reflect] attribute DOMString size;
+ [CEReactions, Reflect] attribute DOMString width;
};
partial interface HTMLHtmlElement {
- [CEReactions] attribute DOMString version;
+ [CEReactions, Reflect] attribute DOMString version;
};
partial interface HTMLIFrameElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString scrolling;
- [CEReactions] attribute DOMString frameBorder;
- [CEReactions] attribute USVString longDesc;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString scrolling;
+ [CEReactions, Reflect] attribute DOMString frameBorder;
+ [CEReactions, ReflectURL] attribute USVString longDesc;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginHeight;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginWidth;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginHeight;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginWidth;
};
partial interface HTMLImageElement {
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute USVString lowsrc;
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute unsigned long hspace;
- [CEReactions] attribute unsigned long vspace;
- [CEReactions] attribute USVString longDesc;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, ReflectURL] attribute USVString lowsrc;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute unsigned long hspace;
+ [CEReactions, Reflect] attribute unsigned long vspace;
+ [CEReactions, ReflectURL] attribute USVString longDesc;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString border;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString border;
};
partial interface HTMLInputElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString useMap;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString useMap;
};
partial interface HTMLLegendElement {
- [CEReactions] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString align;
};
partial interface HTMLLIElement {
- [CEReactions] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString type;
};
partial interface HTMLLinkElement {
- [CEReactions] attribute DOMString charset;
- [CEReactions] attribute DOMString rev;
- [CEReactions] attribute DOMString target;
+ [CEReactions, Reflect] attribute DOMString charset;
+ [CEReactions, Reflect] attribute DOMString rev;
+ [CEReactions, Reflect] attribute DOMString target;
};
partial interface HTMLMenuElement {
- [CEReactions] attribute boolean compact;
+ [CEReactions, Reflect] attribute boolean compact;
};
partial interface HTMLMetaElement {
- [CEReactions] attribute DOMString scheme;
+ [CEReactions, Reflect] attribute DOMString scheme;
};
partial interface HTMLObjectElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString archive;
- [CEReactions] attribute DOMString code;
- [CEReactions] attribute boolean declare;
- [CEReactions] attribute unsigned long hspace;
- [CEReactions] attribute DOMString standby;
- [CEReactions] attribute unsigned long vspace;
- [CEReactions] attribute DOMString codeBase;
- [CEReactions] attribute DOMString codeType;
- [CEReactions] attribute DOMString useMap;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString archive;
+ [CEReactions, Reflect] attribute DOMString code;
+ [CEReactions, Reflect] attribute boolean declare;
+ [CEReactions, Reflect] attribute unsigned long hspace;
+ [CEReactions, Reflect] attribute DOMString standby;
+ [CEReactions, Reflect] attribute unsigned long vspace;
+ [CEReactions, ReflectURL] attribute DOMString codeBase;
+ [CEReactions, Reflect] attribute DOMString codeType;
+ [CEReactions, Reflect] attribute DOMString useMap;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString border;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString border;
};
partial interface HTMLOListElement {
- [CEReactions] attribute boolean compact;
+ [CEReactions, Reflect] attribute boolean compact;
};
partial interface HTMLParagraphElement {
- [CEReactions] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString align;
};
[Exposed=Window]
interface HTMLParamElement : HTMLElement {
[HTMLConstructor] constructor();
- [CEReactions] attribute DOMString name;
- [CEReactions] attribute DOMString value;
- [CEReactions] attribute DOMString type;
- [CEReactions] attribute DOMString valueType;
+ [CEReactions, Reflect] attribute DOMString name;
+ [CEReactions, Reflect] attribute DOMString value;
+ [CEReactions, Reflect] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString valueType;
};
partial interface HTMLPreElement {
- [CEReactions] attribute long width;
+ [CEReactions, Reflect] attribute long width;
};
partial interface HTMLStyleElement {
- [CEReactions] attribute DOMString type;
+ [CEReactions, Reflect] attribute DOMString type;
};
partial interface HTMLScriptElement {
- [CEReactions] attribute DOMString charset;
- [CEReactions] attribute DOMString event;
- [CEReactions] attribute DOMString htmlFor;
+ [CEReactions, Reflect] attribute DOMString charset;
+ [CEReactions, Reflect] attribute DOMString event;
+ [CEReactions, Reflect="for"] attribute DOMString htmlFor;
};
partial interface HTMLTableElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString border;
- [CEReactions] attribute DOMString frame;
- [CEReactions] attribute DOMString rules;
- [CEReactions] attribute DOMString summary;
- [CEReactions] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString border;
+ [CEReactions, Reflect] attribute DOMString frame;
+ [CEReactions, Reflect] attribute DOMString rules;
+ [CEReactions, Reflect] attribute DOMString summary;
+ [CEReactions, Reflect] attribute DOMString width;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString cellPadding;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString cellSpacing;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString cellPadding;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString cellSpacing;
};
partial interface HTMLTableSectionElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString ch;
- [CEReactions] attribute DOMString chOff;
- [CEReactions] attribute DOMString vAlign;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect="char"] attribute DOMString ch;
+ [CEReactions, Reflect="charoff"] attribute DOMString chOff;
+ [CEReactions, Reflect] attribute DOMString vAlign;
};
partial interface HTMLTableCellElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString axis;
- [CEReactions] attribute DOMString height;
- [CEReactions] attribute DOMString width;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect] attribute DOMString axis;
+ [CEReactions, Reflect] attribute DOMString height;
+ [CEReactions, Reflect] attribute DOMString width;
- [CEReactions] attribute DOMString ch;
- [CEReactions] attribute DOMString chOff;
- [CEReactions] attribute boolean noWrap;
- [CEReactions] attribute DOMString vAlign;
+ [CEReactions, Reflect="char"] attribute DOMString ch;
+ [CEReactions, Reflect="charoff"] attribute DOMString chOff;
+ [CEReactions, Reflect] attribute boolean noWrap;
+ [CEReactions, Reflect] attribute DOMString vAlign;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor;
};
partial interface HTMLTableRowElement {
- [CEReactions] attribute DOMString align;
- [CEReactions] attribute DOMString ch;
- [CEReactions] attribute DOMString chOff;
- [CEReactions] attribute DOMString vAlign;
+ [CEReactions, Reflect] attribute DOMString align;
+ [CEReactions, Reflect="char"] attribute DOMString ch;
+ [CEReactions, Reflect="charoff"] attribute DOMString chOff;
+ [CEReactions, Reflect] attribute DOMString vAlign;
- [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor;
+ [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor;
};
partial interface HTMLUListElement {
- [CEReactions] attribute boolean compact;
- [CEReactions] attribute DOMString type;
+ [CEReactions, Reflect] attribute boolean compact;
+ [CEReactions, Reflect] attribute DOMString type;
};
partial interface Document {
diff --git a/test/fixtures/wpt/interfaces/referrer-policy.idl b/test/fixtures/wpt/interfaces/referrer-policy.idl
new file mode 100644
index 00000000000000..0ef9a1fdecc872
--- /dev/null
+++ b/test/fixtures/wpt/interfaces/referrer-policy.idl
@@ -0,0 +1,16 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Referrer Policy (https://w3c.github.io/webappsec-referrer-policy/)
+
+enum ReferrerPolicy {
+ "",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "same-origin",
+ "origin",
+ "strict-origin",
+ "origin-when-cross-origin",
+ "strict-origin-when-cross-origin",
+ "unsafe-url"
+};
diff --git a/test/fixtures/wpt/interfaces/resource-timing.idl b/test/fixtures/wpt/interfaces/resource-timing.idl
index 66f2841d744af3..fd4033ce4d6a32 100644
--- a/test/fixtures/wpt/interfaces/resource-timing.idl
+++ b/test/fixtures/wpt/interfaces/resource-timing.idl
@@ -22,12 +22,17 @@ interface PerformanceResourceTiming : PerformanceEntry {
readonly attribute DOMHighResTimeStamp firstInterimResponseStart;
readonly attribute DOMHighResTimeStamp responseStart;
readonly attribute DOMHighResTimeStamp responseEnd;
+ readonly attribute DOMHighResTimeStamp workerRouterEvaluationStart;
+ readonly attribute DOMHighResTimeStamp workerCacheLookupStart;
+ readonly attribute DOMString workerMatchedRouterSource;
+ readonly attribute DOMString workerFinalRouterSource;
readonly attribute unsigned long long transferSize;
readonly attribute unsigned long long encodedBodySize;
readonly attribute unsigned long long decodedBodySize;
readonly attribute unsigned short responseStatus;
readonly attribute RenderBlockingStatusType renderBlockingStatus;
readonly attribute DOMString contentType;
+ readonly attribute DOMString contentEncoding;
[Default] object toJSON();
};
diff --git a/test/fixtures/wpt/interfaces/streams.idl b/test/fixtures/wpt/interfaces/streams.idl
index ab9be033e43ba0..8abc8f5cfda9fe 100644
--- a/test/fixtures/wpt/interfaces/streams.idl
+++ b/test/fixtures/wpt/interfaces/streams.idl
@@ -17,7 +17,7 @@ interface ReadableStream {
Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {});
sequence tee();
- async iterable(optional ReadableStreamIteratorOptions options = {});
+ async_iterable(optional ReadableStreamIteratorOptions options = {});
};
typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader;
diff --git a/test/fixtures/wpt/interfaces/web-locks.idl b/test/fixtures/wpt/interfaces/web-locks.idl
index 14bc3a22cc395f..00648cc3b1e5f4 100644
--- a/test/fixtures/wpt/interfaces/web-locks.idl
+++ b/test/fixtures/wpt/interfaces/web-locks.idl
@@ -1,3 +1,8 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into webref
+// (https://github.com/w3c/webref)
+// Source: Web Locks API (https://w3c.github.io/web-locks/)
+
[SecureContext]
interface mixin NavigatorLocks {
readonly attribute LockManager locks;
@@ -5,7 +10,7 @@ interface mixin NavigatorLocks {
Navigator includes NavigatorLocks;
WorkerNavigator includes NavigatorLocks;
-[SecureContext, Exposed=(Window,Worker)]
+[SecureContext, Exposed=(Window,Worker,SharedStorageWorklet)]
interface LockManager {
Promise request(DOMString name,
LockGrantedCallback callback);
@@ -38,7 +43,7 @@ dictionary LockInfo {
DOMString clientId;
};
-[SecureContext, Exposed=(Window,Worker)]
+[SecureContext, Exposed=(Window,Worker,SharedStorageWorklet)]
interface Lock {
readonly attribute DOMString name;
readonly attribute LockMode mode;
diff --git a/test/fixtures/wpt/interfaces/webcrypto.idl b/test/fixtures/wpt/interfaces/webcrypto.idl
deleted file mode 100644
index ff7a89cd0d51be..00000000000000
--- a/test/fixtures/wpt/interfaces/webcrypto.idl
+++ /dev/null
@@ -1,262 +0,0 @@
-// GENERATED CONTENT - DO NOT EDIT
-// Content was automatically extracted by Reffy into webref
-// (https://github.com/w3c/webref)
-// Source: Web Cryptography API (https://w3c.github.io/webcrypto/)
-
-partial interface mixin WindowOrWorkerGlobalScope {
- [SameObject] readonly attribute Crypto crypto;
-};
-
-[Exposed=(Window,Worker)]
-interface Crypto {
- [SecureContext] readonly attribute SubtleCrypto subtle;
- ArrayBufferView getRandomValues(ArrayBufferView array);
- [SecureContext] DOMString randomUUID();
-};
-
-typedef (object or DOMString) AlgorithmIdentifier;
-
-typedef AlgorithmIdentifier HashAlgorithmIdentifier;
-
-dictionary Algorithm {
- required DOMString name;
-};
-
-dictionary KeyAlgorithm {
- required DOMString name;
-};
-
-enum KeyType { "public", "private", "secret" };
-
-enum KeyUsage { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey" };
-
-[SecureContext,Exposed=(Window,Worker),Serializable]
-interface CryptoKey {
- readonly attribute KeyType type;
- readonly attribute boolean extractable;
- readonly attribute object algorithm;
- readonly attribute object usages;
-};
-
-enum KeyFormat { "raw", "spki", "pkcs8", "jwk" };
-
-[SecureContext,Exposed=(Window,Worker)]
-interface SubtleCrypto {
- Promise encrypt(
- AlgorithmIdentifier algorithm,
- CryptoKey key,
- BufferSource data
- );
- Promise decrypt(
- AlgorithmIdentifier algorithm,
- CryptoKey key,
- BufferSource data
- );
- Promise sign(
- AlgorithmIdentifier algorithm,
- CryptoKey key,
- BufferSource data
- );
- Promise verify(
- AlgorithmIdentifier algorithm,
- CryptoKey key,
- BufferSource signature,
- BufferSource data
- );
- Promise digest(
- AlgorithmIdentifier algorithm,
- BufferSource data
- );
-
- Promise<(CryptoKey or CryptoKeyPair)> generateKey(
- AlgorithmIdentifier algorithm,
- boolean extractable,
- sequence keyUsages
- );
- Promise deriveKey(
- AlgorithmIdentifier algorithm,
- CryptoKey baseKey,
- AlgorithmIdentifier derivedKeyType,
- boolean extractable,
- sequence keyUsages
- );
- Promise deriveBits(
- AlgorithmIdentifier algorithm,
- CryptoKey baseKey,
- optional unsigned long? length = null
- );
-
- Promise importKey(
- KeyFormat format,
- (BufferSource or JsonWebKey) keyData,
- AlgorithmIdentifier algorithm,
- boolean extractable,
- sequence keyUsages
- );
- Promise<(ArrayBuffer or JsonWebKey)> exportKey(
- KeyFormat format,
- CryptoKey key
- );
-
- Promise wrapKey(
- KeyFormat format,
- CryptoKey key,
- CryptoKey wrappingKey,
- AlgorithmIdentifier wrapAlgorithm
- );
- Promise unwrapKey(
- KeyFormat format,
- BufferSource wrappedKey,
- CryptoKey unwrappingKey,
- AlgorithmIdentifier unwrapAlgorithm,
- AlgorithmIdentifier unwrappedKeyAlgorithm,
- boolean extractable,
- sequence keyUsages
- );
-};
-
-dictionary RsaOtherPrimesInfo {
- // The following fields are defined in Section 6.3.2.7 of JSON Web Algorithms
- DOMString r;
- DOMString d;
- DOMString t;
-};
-
-dictionary JsonWebKey {
- // The following fields are defined in Section 3.1 of JSON Web Key
- DOMString kty;
- DOMString use;
- sequence key_ops;
- DOMString alg;
-
- // The following fields are defined in JSON Web Key Parameters Registration
- boolean ext;
-
- // The following fields are defined in Section 6 of JSON Web Algorithms
- DOMString crv;
- DOMString x;
- DOMString y;
- DOMString d;
- DOMString n;
- DOMString e;
- DOMString p;
- DOMString q;
- DOMString dp;
- DOMString dq;
- DOMString qi;
- sequence oth;
- DOMString k;
-};
-
-typedef Uint8Array BigInteger;
-
-dictionary CryptoKeyPair {
- CryptoKey publicKey;
- CryptoKey privateKey;
-};
-
-dictionary RsaKeyGenParams : Algorithm {
- required [EnforceRange] unsigned long modulusLength;
- required BigInteger publicExponent;
-};
-
-dictionary RsaHashedKeyGenParams : RsaKeyGenParams {
- required HashAlgorithmIdentifier hash;
-};
-
-dictionary RsaKeyAlgorithm : KeyAlgorithm {
- required unsigned long modulusLength;
- required BigInteger publicExponent;
-};
-
-dictionary RsaHashedKeyAlgorithm : RsaKeyAlgorithm {
- required KeyAlgorithm hash;
-};
-
-dictionary RsaHashedImportParams : Algorithm {
- required HashAlgorithmIdentifier hash;
-};
-
-dictionary RsaPssParams : Algorithm {
- required [EnforceRange] unsigned long saltLength;
-};
-
-dictionary RsaOaepParams : Algorithm {
- BufferSource label;
-};
-
-dictionary EcdsaParams : Algorithm {
- required HashAlgorithmIdentifier hash;
-};
-
-typedef DOMString NamedCurve;
-
-dictionary EcKeyGenParams : Algorithm {
- required NamedCurve namedCurve;
-};
-
-dictionary EcKeyAlgorithm : KeyAlgorithm {
- required NamedCurve namedCurve;
-};
-
-dictionary EcKeyImportParams : Algorithm {
- required NamedCurve namedCurve;
-};
-
-dictionary EcdhKeyDeriveParams : Algorithm {
- required CryptoKey public;
-};
-
-dictionary AesCtrParams : Algorithm {
- required BufferSource counter;
- required [EnforceRange] octet length;
-};
-
-dictionary AesKeyAlgorithm : KeyAlgorithm {
- required unsigned short length;
-};
-
-dictionary AesKeyGenParams : Algorithm {
- required [EnforceRange] unsigned short length;
-};
-
-dictionary AesDerivedKeyParams : Algorithm {
- required [EnforceRange] unsigned short length;
-};
-
-dictionary AesCbcParams : Algorithm {
- required BufferSource iv;
-};
-
-dictionary AesGcmParams : Algorithm {
- required BufferSource iv;
- BufferSource additionalData;
- [EnforceRange] octet tagLength;
-};
-
-dictionary HmacImportParams : Algorithm {
- required HashAlgorithmIdentifier hash;
- [EnforceRange] unsigned long length;
-};
-
-dictionary HmacKeyAlgorithm : KeyAlgorithm {
- required KeyAlgorithm hash;
- required unsigned long length;
-};
-
-dictionary HmacKeyGenParams : Algorithm {
- required HashAlgorithmIdentifier hash;
- [EnforceRange] unsigned long length;
-};
-
-dictionary HkdfParams : Algorithm {
- required HashAlgorithmIdentifier hash;
- required BufferSource salt;
- required BufferSource info;
-};
-
-dictionary Pbkdf2Params : Algorithm {
- required BufferSource salt;
- required [EnforceRange] unsigned long iterations;
- required HashAlgorithmIdentifier hash;
-};
diff --git a/test/fixtures/wpt/interfaces/webidl.idl b/test/fixtures/wpt/interfaces/webidl.idl
index f3db91096ac1be..651c1922115026 100644
--- a/test/fixtures/wpt/interfaces/webidl.idl
+++ b/test/fixtures/wpt/interfaces/webidl.idl
@@ -3,6 +3,19 @@
// (https://github.com/w3c/webref)
// Source: Web IDL Standard (https://webidl.spec.whatwg.org/)
+[Exposed=*, Serializable]
+interface QuotaExceededError : DOMException {
+ constructor(optional DOMString message = "", optional QuotaExceededErrorOptions options = {});
+
+ readonly attribute double? quota;
+ readonly attribute double? requested;
+};
+
+dictionary QuotaExceededErrorOptions {
+ double quota;
+ double requested;
+};
+
typedef (Int8Array or Int16Array or Int32Array or
Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
BigInt64Array or BigUint64Array or
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index 50173e71b1b9d7..6a06e26d28a4b5 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -23,6 +23,10 @@
"commit": "1ac8deee082ecfb5d3b6f9c56cf9d1688a2fc218",
"path": "encoding"
},
+ "fetch/api": {
+ "commit": "75b487b9ed041ee69e4a240ef8d675fac8845603",
+ "path": "fetch/api"
+ },
"fetch/data-urls/resources": {
"commit": "7c79d998ff42e52de90290cb847d1b515b3b58f7",
"path": "fetch/data-urls/resources"
@@ -52,7 +56,7 @@
"path": "html/webappapis/timers"
},
"interfaces": {
- "commit": "e1b27be06b43787a001b7297c4e0fabdd276560f",
+ "commit": "b619cb7f23b949daab02c576ac299036ade097b5",
"path": "interfaces"
},
"performance-timeline": {
diff --git a/test/wpt/status/fetch/api.json b/test/wpt/status/fetch/api.json
new file mode 100644
index 00000000000000..cd19a109137d39
--- /dev/null
+++ b/test/wpt/status/fetch/api.json
@@ -0,0 +1,356 @@
+{
+ "abort/cache.https.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "abort/general.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "abort/request.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/accept-header.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/conditional-get.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/error-after-response.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/header-value-combining.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/header-value-null-byte.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/http-response-code.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/integrity.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/keepalive.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/mediasource.window.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/mode-no-cors.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/mode-same-origin.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/referrer.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-forbidden-headers.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-head.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-headers-case.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-headers-nonascii.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-headers.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-private-network-headers.tentative.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-referrer.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-upload.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/request-upload.h2.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/response-null-body.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/response-url.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/scheme-about.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/scheme-blob.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/scheme-data.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/scheme-others.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/status.h2.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/stream-response.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/stream-safe-creation.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "basic/text-utf8.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-basic.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-cookies-redirect.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-cookies.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-expose-star.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-filtering.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-keepalive.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-multiple-origins.sub.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-no-preflight.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-origin.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-cache.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-not-cors-safelisted.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-redirect.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-referrer.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-response-validation.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-star.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight-status.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-preflight.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-redirect-credentials.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-redirect-preflight.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "cors/cors-redirect.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "crashtests/huge-fetch.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "credentials/authentication-basic.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "credentials/authentication-redirection.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "credentials/cookies.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "headers/header-setcookie.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "headers/header-values-normalize.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "headers/header-values.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "headers/headers-no-cors.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "idlharness.https.any.js": {
+ "fail": {
+ "expected": [
+ "Headers interface: existence and properties of interface object",
+ "Request interface: existence and properties of interface object",
+ "Response interface: existence and properties of interface object",
+ "FetchLaterResult interface: existence and properties of interface object",
+ "FetchLaterResult interface object length",
+ "FetchLaterResult interface object name",
+ "FetchLaterResult interface: existence and properties of interface prototype object",
+ "FetchLaterResult interface: existence and properties of interface prototype object's \"constructor\" property",
+ "FetchLaterResult interface: existence and properties of interface prototype object's @@unscopables property",
+ "FetchLaterResult interface: attribute activated",
+ "Window interface: operation fetchLater(RequestInfo, optional DeferredRequestInit)",
+ "Window interface: window must inherit property \"fetchLater(RequestInfo, optional DeferredRequestInit)\" with the proper type",
+ "Window interface: calling fetchLater(RequestInfo, optional DeferredRequestInit) on window with too few arguments must throw TypeError",
+ "Window interface: window must inherit property \"fetch(RequestInfo, optional RequestInit)\" with the proper type",
+ "Window interface: calling fetch(RequestInfo, optional RequestInit) on window with too few arguments must throw TypeError"
+ ]
+ }
+ },
+ "redirect/redirect-back-to-original-origin.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-count.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-empty-location.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-keepalive.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-keepalive.https.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-location-escape.tentative.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-location.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-method.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-mode.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-origin.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-referrer-override.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-referrer.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-schemes.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-to-dataurl.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "redirect/redirect-upload.h2.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/multi-globals/construct-in-detached-frame.window.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-bad-port.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-default-conditional.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-default.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-force-cache.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-no-cache.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-no-store.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-only-if-cached.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-cache-reload.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-consume-empty.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-consume.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-disturbed.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-error.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-headers.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-init-002.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-init-priority.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-init-stream.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-keepalive.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "request/request-structure.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/json.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-arraybuffer-realm.window.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-blob-realm.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-cancel-stream.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-clone-iframe.window.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-clone.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-consume-empty.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-consume-stream.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-headers-guard.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-stream-disturbed-1.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-stream-disturbed-2.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-stream-disturbed-3.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-stream-disturbed-4.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-stream-disturbed-5.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ },
+ "response/response-stream-with-broken-then.any.js": {
+ "skip": "Requires a WPT HTTP server"
+ }
+}
diff --git a/test/wpt/test-fetch.js b/test/wpt/test-fetch.js
new file mode 100644
index 00000000000000..3dd7e4fc5f470c
--- /dev/null
+++ b/test/wpt/test-fetch.js
@@ -0,0 +1,7 @@
+'use strict';
+
+const { WPTRunner } = require('../common/wpt');
+
+const runner = new WPTRunner('fetch/api');
+runner.pretendGlobalThisAs('Window');
+runner.runJsTests();
\ No newline at end of file