From 45ee342d2b2b0bb0c4a628d886044672b4ef85aa Mon Sep 17 00:00:00 2001 From: vitkarpov Date: Sun, 22 Feb 2026 15:02:00 +0000 Subject: [PATCH] test: enable wpt fetch/api idlharness tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables fetch/api/idlharness.https.any.js from the WPT suite in Node's test runner. This test formally documents the property descriptor regression reported in #45099 — Headers, Request, and Response are currently exposed on globalThis as getter/setter accessor pairs rather than plain { value, writable, enumerable: false, configurable: true } data properties as required by WebIDL. --- test/common/wpt.js | 1 + test/common/wpt/worker.js | 1 + test/fixtures/wpt/README.md | 3 +- .../wpt/fetch/api/abort/WEB_FEATURES.yml | 3 + .../wpt/fetch/api/abort/cache.https.any.js | 47 ++ .../fetch/api/abort/destroyed-context.html | 27 + .../wpt/fetch/api/abort/general.any.js | 572 ++++++++++++++ .../wpt/fetch/api/abort/keepalive.html | 85 +++ .../wpt/fetch/api/abort/request.any.js | 85 +++ .../serviceworker-intercepted.https.html | 212 ++++++ .../wpt/fetch/api/basic/WEB_FEATURES.yml | 21 + .../wpt/fetch/api/basic/accept-header.any.js | 34 + .../fetch/api/basic/block-mime-as-script.html | 43 ++ .../fetch/api/basic/conditional-get.any.js | 38 + .../api/basic/error-after-response.any.js | 24 + test/fixtures/wpt/fetch/api/basic/gc.any.js | 19 + .../api/basic/header-value-combining.any.js | 15 + .../api/basic/header-value-null-byte.any.js | 5 + .../wpt/fetch/api/basic/historical.any.js | 17 + .../fetch/api/basic/http-response-code.any.js | 14 + .../wpt/fetch/api/basic/integrity.sub.any.js | 87 +++ .../wpt/fetch/api/basic/keepalive.any.js | 77 ++ .../wpt/fetch/api/basic/mediasource.window.js | 5 + .../fetch/api/basic/mode-no-cors.sub.any.js | 29 + .../fetch/api/basic/mode-same-origin.any.js | 28 + .../wpt/fetch/api/basic/referrer.any.js | 29 + .../basic/request-forbidden-headers.any.js | 82 ++ .../wpt/fetch/api/basic/request-head.any.js | 6 + .../api/basic/request-headers-case.any.js | 13 + .../api/basic/request-headers-nonascii.any.js | 29 + .../fetch/api/basic/request-headers.any.js | 83 +++ ...t-private-network-headers.tentative.any.js | 18 + .../request-referrer-redirected-worker.html | 17 + .../fetch/api/basic/request-referrer.any.js | 24 + .../wpt/fetch/api/basic/request-upload.any.js | 139 ++++ .../fetch/api/basic/request-upload.h2.any.js | 209 ++++++ .../fetch/api/basic/response-null-body.any.js | 38 + .../fetch/api/basic/response-url.sub.any.js | 16 + .../wpt/fetch/api/basic/scheme-about.any.js | 26 + .../fetch/api/basic/scheme-blob.sub.any.js | 125 ++++ .../wpt/fetch/api/basic/scheme-data.any.js | 43 ++ .../fetch/api/basic/scheme-others.sub.any.js | 31 + .../wpt/fetch/api/basic/status.h2.any.js | 17 + .../fetch/api/basic/stream-response.any.js | 40 + .../api/basic/stream-safe-creation.any.js | 54 ++ .../wpt/fetch/api/basic/text-utf8.any.js | 74 ++ .../wpt/fetch/api/basic/url-parsing.sub.html | 33 + .../wpt/fetch/api/body/WEB_FEATURES.yml | 3 + .../fixtures/wpt/fetch/api/body/cloned-any.js | 50 ++ .../wpt/fetch/api/body/formdata.any.js | 25 + .../wpt/fetch/api/body/mime-type.any.js | 127 ++++ .../wpt/fetch/api/cors/WEB_FEATURES.yml | 3 + .../wpt/fetch/api/cors/cors-basic.any.js | 43 ++ .../api/cors/cors-cookies-redirect.any.js | 49 ++ .../wpt/fetch/api/cors/cors-cookies.any.js | 56 ++ .../api/cors/cors-expose-star.sub.any.js | 41 + .../fetch/api/cors/cors-filtering.sub.any.js | 65 ++ .../wpt/fetch/api/cors/cors-keepalive.any.js | 116 +++ .../api/cors/cors-multiple-origins.sub.any.js | 22 + .../fetch/api/cors/cors-no-preflight.any.js | 41 + .../wpt/fetch/api/cors/cors-origin.any.js | 51 ++ .../api/cors/cors-preflight-cache.any.js | 46 ++ .../cors-preflight-not-cors-safelisted.any.js | 19 + .../api/cors/cors-preflight-redirect.any.js | 37 + .../api/cors/cors-preflight-referrer.any.js | 51 ++ .../cors-preflight-response-validation.any.js | 33 + .../fetch/api/cors/cors-preflight-star.any.js | 86 +++ .../api/cors/cors-preflight-status.any.js | 37 + .../wpt/fetch/api/cors/cors-preflight.any.js | 62 ++ .../api/cors/cors-redirect-credentials.any.js | 52 ++ .../api/cors/cors-redirect-preflight.any.js | 46 ++ .../wpt/fetch/api/cors/cors-redirect.any.js | 42 ++ .../wpt/fetch/api/cors/data-url-iframe.html | 58 ++ .../api/cors/data-url-shared-worker.html | 53 ++ .../wpt/fetch/api/cors/data-url-worker.html | 50 ++ .../fetch/api/cors/resources/corspreflight.js | 58 ++ .../cors/resources/not-cors-safelisted.json | 13 + .../wpt/fetch/api/cors/sandboxed-iframe.html | 14 + .../wpt/fetch/api/crashtests/WEB_FEATURES.yml | 3 + .../aborted-fetch-response.https.html | 11 + .../api/crashtests/body-window-destroy.html | 11 + .../fetch/api/crashtests/huge-fetch.any.js | 16 + .../wpt/fetch/api/crashtests/request.html | 8 + .../fetch/api/credentials/WEB_FEATURES.yml | 10 + .../credentials/authentication-basic.any.js | 17 + .../authentication-redirection.any.js | 29 + .../wpt/fetch/api/credentials/cookies.any.js | 49 ++ .../wpt/fetch/api/headers/WEB_FEATURES.yml | 3 + .../fetch/api/headers/header-setcookie.any.js | 266 +++++++ .../headers/header-values-normalize.any.js | 73 ++ .../fetch/api/headers/header-values.any.js | 64 ++ .../fetch/api/headers/headers-basic.any.js | 275 +++++++ .../fetch/api/headers/headers-casing.any.js | 54 ++ .../fetch/api/headers/headers-combine.any.js | 66 ++ .../fetch/api/headers/headers-errors.any.js | 96 +++ .../fetch/api/headers/headers-no-cors.any.js | 59 ++ .../api/headers/headers-normalize.any.js | 56 ++ .../fetch/api/headers/headers-record.any.js | 357 +++++++++ .../api/headers/headers-structure.any.js | 20 + .../wpt/fetch/api/idlharness.https.any.js | 21 + .../wpt/fetch/api/policies/WEB_FEATURES.yml | 3 + .../api/policies/csp-blocked-worker.html | 16 + .../wpt/fetch/api/policies/csp-blocked.html | 15 + .../api/policies/csp-blocked.html.headers | 1 + .../wpt/fetch/api/policies/csp-blocked.js | 13 + .../fetch/api/policies/csp-blocked.js.headers | 1 + .../wpt/fetch/api/policies/nested-policy.js | 1 + .../api/policies/nested-policy.js.headers | 1 + ...rrer-no-referrer-service-worker.https.html | 18 + .../policies/referrer-no-referrer-worker.html | 17 + .../api/policies/referrer-no-referrer.html | 15 + .../referrer-no-referrer.html.headers | 1 + .../api/policies/referrer-no-referrer.js | 19 + .../policies/referrer-no-referrer.js.headers | 1 + .../referrer-origin-service-worker.https.html | 18 + ...hen-cross-origin-service-worker.https.html | 17 + ...errer-origin-when-cross-origin-worker.html | 16 + .../referrer-origin-when-cross-origin.html | 16 + ...rrer-origin-when-cross-origin.html.headers | 1 + .../referrer-origin-when-cross-origin.js | 21 + ...ferrer-origin-when-cross-origin.js.headers | 1 + .../api/policies/referrer-origin-worker.html | 17 + .../fetch/api/policies/referrer-origin.html | 16 + .../api/policies/referrer-origin.html.headers | 1 + .../wpt/fetch/api/policies/referrer-origin.js | 30 + .../api/policies/referrer-origin.js.headers | 1 + ...errer-unsafe-url-service-worker.https.html | 18 + .../policies/referrer-unsafe-url-worker.html | 17 + .../api/policies/referrer-unsafe-url.html | 16 + .../policies/referrer-unsafe-url.html.headers | 1 + .../fetch/api/policies/referrer-unsafe-url.js | 21 + .../policies/referrer-unsafe-url.js.headers | 1 + .../wpt/fetch/api/redirect/WEB_FEATURES.yml | 3 + .../redirect-back-to-original-origin.any.js | 38 + .../fetch/api/redirect/redirect-count.any.js | 51 ++ .../redirect/redirect-empty-location.any.js | 21 + .../api/redirect/redirect-keepalive.any.js | 35 + .../redirect/redirect-keepalive.https.any.js | 18 + .../redirect-location-escape.tentative.any.js | 46 ++ .../api/redirect/redirect-location.any.js | 73 ++ .../fetch/api/redirect/redirect-method.any.js | 112 +++ .../fetch/api/redirect/redirect-mode.any.js | 59 ++ .../fetch/api/redirect/redirect-origin.any.js | 68 ++ .../redirect-referrer-override.any.js | 104 +++ .../api/redirect/redirect-referrer.any.js | 66 ++ .../api/redirect/redirect-schemes.any.js | 19 + .../api/redirect/redirect-to-dataurl.any.js | 28 + .../api/redirect/redirect-upload.h2.any.js | 33 + .../wpt/fetch/api/request/WEB_FEATURES.yml | 8 + .../fetch-destination-frame.https.html | 51 ++ .../fetch-destination-iframe.https.html | 51 ++ ...fetch-destination-no-load-event.https.html | 138 ++++ .../fetch-destination-prefetch.https.html | 46 ++ .../fetch-destination-worker.https.html | 60 ++ .../destination/fetch-destination.https.html | 485 ++++++++++++ .../api/request/destination/resources/dummy | 0 .../request/destination/resources/dummy.css | 0 .../request/destination/resources/dummy.es | 0 .../destination/resources/dummy.es.headers | 1 + .../request/destination/resources/dummy.html | 0 .../request/destination/resources/dummy.json | 1 + .../request/destination/resources/dummy.png | Bin 0 -> 18299 bytes .../request/destination/resources/dummy.ttf | Bin 0 -> 2528 bytes .../destination/resources/dummy_audio.mp3 | Bin 0 -> 20498 bytes .../destination/resources/dummy_audio.oga | Bin 0 -> 18541 bytes .../destination/resources/dummy_video.mp4 | Bin 0 -> 67369 bytes .../destination/resources/dummy_video.webm | Bin 0 -> 96902 bytes .../destination/resources/empty.https.html | 0 .../fetch-destination-worker-frame.js | 20 + .../fetch-destination-worker-iframe.js | 20 + .../fetch-destination-worker-no-load-event.js | 20 + .../resources/fetch-destination-worker.js | 12 + .../resources/import-declaration-type-css.js | 1 + .../resources/import-declaration-type-json.js | 1 + .../request/destination/resources/importer.js | 1 + .../fetch/api/request/forbidden-method.any.js | 13 + .../construct-in-detached-frame.window.js | 11 + .../multi-globals/current/current.html | 3 + .../multi-globals/incumbent/incumbent.html | 14 + .../request/multi-globals/url-parsing.html | 27 + .../fetch/api/request/request-bad-port.any.js | 95 +++ .../request-cache-default-conditional.any.js | 170 +++++ .../api/request/request-cache-default.any.js | 39 + .../request/request-cache-force-cache.any.js | 67 ++ .../api/request/request-cache-no-cache.any.js | 25 + .../api/request/request-cache-no-store.any.js | 37 + .../request-cache-only-if-cached.any.js | 66 ++ .../api/request/request-cache-reload.any.js | 51 ++ .../wpt/fetch/api/request/request-cache.js | 223 ++++++ .../fetch/api/request/request-clone.sub.html | 63 ++ ...uest-constructor-init-body-override.any.js | 37 + .../api/request/request-consume-empty.any.js | 89 +++ .../fetch/api/request/request-consume.any.js | 148 ++++ .../api/request/request-disturbed.any.js | 109 +++ .../fetch/api/request/request-error.any.js | 56 ++ .../wpt/fetch/api/request/request-error.js | 57 ++ .../fetch/api/request/request-headers.any.js | 177 +++++ .../api/request/request-init-001.sub.html | 112 +++ .../fetch/api/request/request-init-002.any.js | 60 ++ .../api/request/request-init-003.sub.html | 84 +++ .../request/request-init-contenttype.any.js | 141 ++++ .../api/request/request-init-priority.any.js | 26 + .../api/request/request-init-stream.any.js | 147 ++++ .../api/request/request-keepalive-quota.html | 97 +++ .../api/request/request-keepalive.any.js | 17 + .../request-reset-attributes.https.html | 96 +++ .../api/request/request-structure.any.js | 143 ++++ .../wpt/fetch/api/request/resources/hello.txt | 1 + .../request-reset-attributes-worker.js | 19 + .../wpt/fetch/api/request/url-encoding.html | 25 + .../wpt/fetch/api/resources/basic.html | 5 + .../wpt/fetch/api/resources/cors-top.txt | 1 + .../fetch/api/resources/cors-top.txt.headers | 1 + .../wpt/fetch/api/resources/data.json | 1 + .../wpt/fetch/api/resources/empty.txt | 0 .../fetch/api/resources/keepalive-helper.js | 199 +++++ .../fetch/api/resources/keepalive-iframe.html | 22 + .../resources/keepalive-redirect-iframe.html | 23 + .../resources/keepalive-redirect-window.html | 42 ++ .../fetch/api/resources/keepalive-worker.js | 15 + .../fetch/api/resources/sandboxed-iframe.html | 34 + .../fetch/api/resources/sw-intercept-abort.js | 19 + .../wpt/fetch/api/resources/sw-intercept.js | 10 + test/fixtures/wpt/fetch/api/resources/top.txt | 1 + .../fixtures/wpt/fetch/api/resources/utils.js | 120 +++ .../wpt/fetch/api/response/WEB_FEATURES.yml | 3 + .../wpt/fetch/api/response/json.any.js | 14 + .../api/response/many-empty-chunks-crash.html | 14 + .../multi-globals/current/current.html | 3 + .../multi-globals/incumbent/incumbent.html | 16 + .../multi-globals/relevant/relevant.html | 2 + .../response/multi-globals/url-parsing.html | 27 + .../response-arraybuffer-realm.window.js | 23 + .../api/response/response-blob-realm.any.js | 24 + .../response-body-read-task-handling.html | 86 +++ .../response/response-cancel-stream.any.js | 64 ++ .../response/response-clone-iframe.window.js | 32 + .../fetch/api/response/response-clone.any.js | 141 ++++ .../response/response-consume-empty.any.js | 88 +++ .../response/response-consume-stream.any.js | 80 ++ .../fetch/api/response/response-consume.html | 317 ++++++++ .../response-error-from-stream.any.js | 61 ++ .../fetch/api/response/response-error.any.js | 27 + .../api/response/response-from-stream.any.js | 23 + .../response/response-headers-guard.any.js | 8 + .../api/response/response-init-001.any.js | 64 ++ .../api/response/response-init-002.any.js | 61 ++ .../response/response-init-contenttype.any.js | 125 ++++ .../api/response/response-static-error.any.js | 22 + .../api/response/response-static-json.any.js | 96 +++ .../response/response-static-redirect.any.js | 40 + .../response/response-stream-bad-chunk.any.js | 25 + .../response-stream-disturbed-1.any.js | 44 ++ .../response-stream-disturbed-2.any.js | 35 + .../response-stream-disturbed-3.any.js | 36 + .../response-stream-disturbed-4.any.js | 35 + .../response-stream-disturbed-5.any.js | 19 + .../response-stream-disturbed-6.any.js | 76 ++ .../response-stream-disturbed-by-pipe.any.js | 17 + .../response-stream-disturbed-util.js | 17 + .../response-stream-with-broken-then.any.js | 117 +++ test/fixtures/wpt/interfaces/dom.idl | 10 +- test/fixtures/wpt/interfaces/fetch.idl | 132 ++++ test/fixtures/wpt/interfaces/html.idl | 702 +++++++++--------- .../wpt/interfaces/referrer-policy.idl | 16 + .../wpt/interfaces/resource-timing.idl | 5 + test/fixtures/wpt/interfaces/streams.idl | 2 +- test/fixtures/wpt/interfaces/web-locks.idl | 9 +- test/fixtures/wpt/interfaces/webcrypto.idl | 262 ------- test/fixtures/wpt/interfaces/webidl.idl | 13 + test/fixtures/wpt/versions.json | 6 +- test/wpt/status/fetch/api.json | 356 +++++++++ test/wpt/test-fetch.js | 7 + 273 files changed, 13526 insertions(+), 606 deletions(-) create mode 100644 test/fixtures/wpt/fetch/api/abort/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/abort/cache.https.any.js create mode 100644 test/fixtures/wpt/fetch/api/abort/destroyed-context.html create mode 100644 test/fixtures/wpt/fetch/api/abort/general.any.js create mode 100644 test/fixtures/wpt/fetch/api/abort/keepalive.html create mode 100644 test/fixtures/wpt/fetch/api/abort/request.any.js create mode 100644 test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html create mode 100644 test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/basic/accept-header.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html create mode 100644 test/fixtures/wpt/fetch/api/basic/conditional-get.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/error-after-response.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/gc.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/historical.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/http-response-code.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/keepalive.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/mediasource.window.js create mode 100644 test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-head.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-headers.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html create mode 100644 test/fixtures/wpt/fetch/api/basic/request-referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-upload.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/response-null-body.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-about.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-data.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/status.h2.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/stream-response.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/text-utf8.any.js create mode 100644 test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html create mode 100644 test/fixtures/wpt/fetch/api/body/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/body/cloned-any.js create mode 100644 test/fixtures/wpt/fetch/api/body/formdata.any.js create mode 100644 test/fixtures/wpt/fetch/api/body/mime-type.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-basic.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/cors/data-url-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html create mode 100644 test/fixtures/wpt/fetch/api/cors/data-url-worker.html create mode 100644 test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js create mode 100644 test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json create mode 100644 test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/crashtests/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html create mode 100644 test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html create mode 100644 test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js create mode 100644 test/fixtures/wpt/fetch/api/crashtests/request.html create mode 100644 test/fixtures/wpt/fetch/api/credentials/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js create mode 100644 test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js create mode 100644 test/fixtures/wpt/fetch/api/credentials/cookies.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/header-values.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-basic.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-casing.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-combine.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-errors.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-record.any.js create mode 100644 test/fixtures/wpt/fetch/api/headers/headers-structure.any.js create mode 100644 test/fixtures/wpt/fetch/api/idlharness.https.any.js create mode 100644 test/fixtures/wpt/fetch/api/policies/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.html create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.js create mode 100644 test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/nested-policy.js create mode 100644 test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js create mode 100644 test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers create mode 100644 test/fixtures/wpt/fetch/api/redirect/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js create mode 100644 test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.css create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/empty.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js create mode 100644 test/fixtures/wpt/fetch/api/request/destination/resources/importer.js create mode 100644 test/fixtures/wpt/fetch/api/request/forbidden-method.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html create mode 100644 test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-bad-port.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-default.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-cache.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-clone.sub.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-consume.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-disturbed.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-error.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-error.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-headers.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-001.sub.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-002.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-003.sub.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-priority.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-init-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-keepalive.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html create mode 100644 test/fixtures/wpt/fetch/api/request/request-structure.any.js create mode 100644 test/fixtures/wpt/fetch/api/request/resources/hello.txt create mode 100644 test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js create mode 100644 test/fixtures/wpt/fetch/api/request/url-encoding.html create mode 100644 test/fixtures/wpt/fetch/api/resources/basic.html create mode 100644 test/fixtures/wpt/fetch/api/resources/cors-top.txt create mode 100644 test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers create mode 100644 test/fixtures/wpt/fetch/api/resources/data.json create mode 100644 test/fixtures/wpt/fetch/api/resources/empty.txt create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-helper.js create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html create mode 100644 test/fixtures/wpt/fetch/api/resources/keepalive-worker.js create mode 100644 test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html create mode 100644 test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js create mode 100644 test/fixtures/wpt/fetch/api/resources/sw-intercept.js create mode 100644 test/fixtures/wpt/fetch/api/resources/top.txt create mode 100644 test/fixtures/wpt/fetch/api/resources/utils.js create mode 100644 test/fixtures/wpt/fetch/api/response/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/fetch/api/response/json.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html create mode 100644 test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html create mode 100644 test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html create mode 100644 test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-clone.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-consume.html create mode 100644 test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-error.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-from-stream.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-init-001.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-init-002.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-static-error.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-static-json.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js create mode 100644 test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js create mode 100644 test/fixtures/wpt/interfaces/fetch.idl create mode 100644 test/fixtures/wpt/interfaces/referrer-policy.idl delete mode 100644 test/fixtures/wpt/interfaces/webcrypto.idl create mode 100644 test/wpt/status/fetch/api.json create mode 100644 test/wpt/test-fetch.js 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 0000000000000000000000000000000000000000..01c9666a8de9d5535615aff830810e5df4b2156f GIT binary patch literal 18299 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 bDgs((CoOJ&@$4~l+kmsZyIqm(x-I_(KvUo& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9023592ef5aa83a03dd6957398897a585062ca57 GIT binary patch literal 2528 zcmds3+iM(E9RAMC>}--IZHkSlbcJb}Hmx+fn~4|=c}SaNsWzb@-3F9mI_^#~3%fJR z?rbg~7_kpEQi_7Nf1zrjK~Q`sQnW2nkwV|M-@%oK-E?>x_(MpM29?acOqAH}eAZX8>i{v90{QD@UK8 z?l;0S4h7BS^-meAn|!xZ@)uj54c;RECHbzRm$TF>nv6e6K2fq3%b3Ch^~cB?u2r(v zkH7ad5c`2P>9SY#cXvOjFn>GsdB|P~`n~De%#NWyu}z}@xPEE<^GzId1b6hq`YrNJ zpl7(G&#mANPHPA{)^6*E!$^@bL|Q0mLqc}TB|Swb8%8pe2%7wX7*!uCHz~QWfyJ-r z7tPW^=Wn!Ro%h$|>{uSdXvUa|I&fOQrS7LPvS9~Z(v&zMA=;65&=C=`DhY|mZ&Kj!3HhbNxDPn<$e@rAG|9>`>_U%Yaa|m@d0+T+9$}A07}*9%}w^Ondom zl3bI=hUcy>--uAx7wLGEX&Kzn_%3s9JFtg_4YL!pi){|FXP{H;ZW!kJ85v$FZVvZc z=7R1t%y+FTKz4K3D>LVrE9j_0Kg^W>kV`b=3OX8csX3V|_H9EhakU|rxWPtGG$xDs zeRLLqoPhkgUkcxKN!R$Lr8Sp8i&%_ketg8+5v}5Y_$i__v?%)`I)-*-GNN_L7dTC! z$#2##gbi9?mv|+j6|{;sB3i|`_#mP+>{8kyItD{YMzl`3g%NltV+j=$Fb4-d3>-ub zhlow2(MK>aNlgJoLYZ6^7Cnmetngdg!aW6>yiIwPzj@l!;1b)kFc{MzW$@m3p1uag z87D`H8(I%iBJ=u;J%|+dLb#J*WgAu=<5fZ*DXp;5R9MY}C{;>IjO(NK5lxbD9RfzY z@=~QR=lI6K+#$nE_oaaWNmxCd;m?tPvxY zJ8xC9c9rx5g?W}XCSRSBcZdsV~Z&>YL1E97mjWcg0TE4dJ(nei;|UEZ=>^4@JlJ9c3=csI~@nRO6C WZTNf5{_GpcUH|0vRERIFfAlvXi@|mP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0091330f1ecd9ba5a70c6e5e6da284a061e2b99f GIT binary patch literal 20498 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7022e75c15ee5263f64bf5c2d86b19b72d2ab5f9 GIT binary patch literal 67369 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%0AxWB;E~=xj7<{Wt4OeKYACt+d&&88fiV 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 O{=nA(AP!glu>TjvI>Ctm literal 0 HcmV?d00001 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