From af9aaea082267040aa4b3989a69abeb5e8913a27 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 1 Dec 2023 15:31:26 -0500 Subject: [PATCH] update spec & wpts (#2482) * update spec & wpts restore fetch error debug time * fix some test failures for others!! * fix some test failures for others!! * fix most remaining failures * fix remaining WPT fail * fix fetching blob * mark websocket WPTs as failing --- lib/core/util.js | 7 +- lib/fetch/body.js | 21 +- lib/fetch/dataURL.js | 8 +- lib/fetch/index.js | 336 ++++++++++++------ lib/fetch/response.js | 6 +- lib/fetch/util.js | 174 ++++++++- lib/websocket/connection.js | 1 + lib/websocket/util.js | 1 + test/wpt/server/server.mjs | 1 + test/wpt/status/fetch.status.json | 8 + test/wpt/status/websockets.status.json | 12 + test/wpt/tests/.azure-pipelines.yml | 30 +- test/wpt/tests/.taskcluster.yml | 4 +- test/wpt/tests/FileAPI/META.yml | 2 +- .../wpt/tests/FileAPI/blob/Blob-stream.any.js | 13 +- test/wpt/tests/common/META.yml | 1 - .../wpt/tests/common/dispatcher/dispatcher.js | 23 +- test/wpt/tests/common/dummy.json | 1 + .../security-features/subresource/video.py | 4 +- test/wpt/tests/common/top-layer.js | 29 ++ .../basic/request-forbidden-headers.any.js | 18 - ...t-private-network-headers.tentative.any.js | 18 + .../fetch/api/basic/url-parsing.sub.html | 33 ++ .../fetch/api/cors/cors-keepalive.any.js | 10 +- .../api/redirect/redirect-keepalive.any.js | 70 +--- .../redirect/redirect-keepalive.https.any.js | 20 ++ .../destination/fetch-destination.https.html | 50 +++ .../request/destination/resources/dummy.css | 0 .../request/destination/resources/dummy.json | 1 + .../resources/import-declaration-type-css.js | 1 + .../resources/import-declaration-type-json.js | 1 + .../fetch/api/request/request-headers.any.js | 1 - .../fetch/api/resources/keepalive-helper.js | 113 +++++- .../fetch/api/resources/keepalive-iframe.html | 7 +- .../tests/fetch/api/resources/stash-put.py | 26 +- test/wpt/tests/fetch/api/resources/utils.js | 15 + .../response/response-consume-stream.any.js | 19 + .../response/response-headers-guard.any.js | 8 + .../api/response/response-static-error.any.js | 12 - .../big-gzip-body.https.any.js | 55 +++ .../content-encoding/resources/big.text.gz | Bin 0 -> 65509 bytes .../resources/big.text.gz.headers | 3 + .../activate-after.tentative.https.window.js | 55 +++ .../basic.tentative.https.window.js | 38 +- .../basic.tentative.https.worker.js | 6 + ...ferrer-when-downgrade.tentative.https.html | 23 ++ ...-referrer-no-referrer.tentative.https.html | 19 + ...gin-when-cross-origin.tentative.https.html | 25 ++ ...eader-referrer-origin.tentative.https.html | 23 ++ ...-referrer-same-origin.tentative.https.html | 24 ++ ...gin-when-cross-origin.tentative.https.html | 24 ++ ...eferrer-strict-origin.tentative.https.html | 24 ++ ...r-referrer-unsafe-url.tentative.https.html | 24 ++ .../iframe.tentative.https.window.js | 65 ++++ .../new-window.tentative.https.window.js | 77 ++++ .../csp-allowed.tentative.https.window.js | 28 ++ .../csp-blocked.tentative.https.window.js | 33 ++ ...irect-to-blocked.tentative.https.window.js | 35 ++ .../quota.tentative.https.window.js | 130 +++++++ .../fetch-later/resources/fetch-later.html | 14 + .../resources/header-referrer-helper.js | 39 ++ ...nd-on-deactivate.tentative.https.window.js | 185 ++++++++++ ...send-after-abort.tentative.https.window.js | 25 ++ ...h-activate-after.tentative.https.window.js | 32 ++ .../send-multiple.tentative.https.window.js} | 12 +- .../fetch/metadata/portal.https.sub.html | 50 --- test/wpt/tests/fetch/orb/resources/utils.js | 89 ++++- .../orb/tentative/content-range.sub.any.js | 39 +- .../orb/tentative/known-mime-type.sub.any.js | 145 ++++---- .../fetch/orb/tentative/nosniff.sub.any.js | 77 ++-- .../fetch/orb/tentative/status.sub.any.js | 39 +- .../tentative/unknown-mime-type.sub.any.js | 52 +-- ...eflight-required.tentative.https.window.js | 16 +- .../iframe.tentative.https.window.js | 13 +- .../iframe.tentative.window.js | 2 +- ...-private-network-access-target.https.html} | 0 ...d-frame-private-network-access.https.html} | 0 ...private-network-access.https.html.headers} | 0 .../resources/opener.html | 11 + .../resources/preflight.py | 17 +- .../resources/support.sub.js | 33 +- .../window-open.tentative.https.window.js | 200 +++++++++++ .../window-open.tentative.window.js | 94 +++++ test/wpt/tests/interfaces/FedCM.idl | 40 ++- .../interfaces/WEBGL_clip_cull_distance.idl | 2 +- .../wpt/tests/interfaces/WEBGL_multi_draw.idl | 20 +- ...aw_instanced_base_vertex_base_instance.idl | 18 +- test/wpt/tests/interfaces/accelerometer.idl | 12 - test/wpt/tests/interfaces/ambient-light.idl | 4 - test/wpt/tests/interfaces/audio-session.idl | 33 ++ .../interfaces/captured-mouse-events.idl | 20 ++ test/wpt/tests/interfaces/clipboard-apis.idl | 8 +- test/wpt/tests/interfaces/close-watcher.idl | 19 - test/wpt/tests/interfaces/cookie-store.idl | 3 + .../interfaces/credential-management.idl | 2 +- .../tests/interfaces/css-anchor-position.idl | 3 + .../wpt/tests/interfaces/css-font-loading.idl | 2 - test/wpt/tests/interfaces/css-fonts.idl | 4 +- test/wpt/tests/interfaces/css-nesting.idl | 10 - .../tests/interfaces/css-toggle.tentative.idl | 51 --- .../tests/interfaces/css-transitions-2.idl | 4 + test/wpt/tests/interfaces/css-typed-om.idl | 9 +- .../interfaces/css-view-transitions-2.idl | 30 ++ .../tests/interfaces/css-view-transitions.idl | 4 - test/wpt/tests/interfaces/cssom.idl | 12 +- .../document-picture-in-picture.idl | 3 +- test/wpt/tests/interfaces/dom.idl | 1 + test/wpt/tests/interfaces/edit-context.idl | 26 +- test/wpt/tests/interfaces/element-capture.idl | 14 + test/wpt/tests/interfaces/encoding.idl | 2 +- test/wpt/tests/interfaces/encrypted-media.idl | 17 + test/wpt/tests/interfaces/event-timing.idl | 2 +- test/wpt/tests/interfaces/fenced-frame.idl | 29 +- test/wpt/tests/interfaces/fetch.idl | 2 +- .../tests/interfaces/file-system-access.idl | 2 +- test/wpt/tests/interfaces/fs.idl | 4 +- test/wpt/tests/interfaces/generic-sensor.idl | 30 -- .../tests/interfaces/geolocation-sensor.idl | 10 - test/wpt/tests/interfaces/gpc-spec.idl | 10 - test/wpt/tests/interfaces/gyroscope.idl | 6 - test/wpt/tests/interfaces/html.idl | 215 ++++++++++- test/wpt/tests/interfaces/image-capture.idl | 2 +- .../interfaces/intersection-observer.idl | 2 + .../tests/interfaces/invokers.tentative.idl | 15 + test/wpt/tests/interfaces/longtasks.idl | 9 + test/wpt/tests/interfaces/magnetometer.idl | 15 - .../interfaces/managed-configuration.idl | 20 ++ test/wpt/tests/interfaces/media-source.idl | 14 +- .../tests/interfaces/mediacapture-streams.idl | 6 +- test/wpt/tests/interfaces/mediasession.idl | 5 +- .../tests/interfaces/navigation-timing.idl | 1 + .../tests/interfaces/orientation-event.idl | 8 +- .../tests/interfaces/orientation-sensor.idl | 7 - .../tests/interfaces/performance-timeline.idl | 2 + .../interfaces/private-network-access.idl | 19 + test/wpt/tests/interfaces/proximity.idl | 6 - .../tests/interfaces/real-world-meshing.idl | 2 +- test/wpt/tests/interfaces/screen-capture.idl | 7 + .../tests/interfaces/scroll-animations.idl | 13 +- .../secure-payment-confirmation.idl | 4 + test/wpt/tests/interfaces/serial.idl | 3 + test/wpt/tests/interfaces/shared-storage.idl | 17 + test/wpt/tests/interfaces/storage-buckets.idl | 14 +- .../interfaces/storage-buckets.tentative.idl | 36 -- test/wpt/tests/interfaces/streams.idl | 10 +- .../tests/interfaces/sub-apps.tentative.idl | 17 - test/wpt/tests/interfaces/trust-token-api.idl | 8 + test/wpt/tests/interfaces/turtledove.idl | 168 +++++++-- test/wpt/tests/interfaces/ua-client-hints.idl | 2 +- test/wpt/tests/interfaces/uievents.idl | 2 + test/wpt/tests/interfaces/urlpattern.idl | 2 +- test/wpt/tests/interfaces/web-animations.idl | 106 +++--- .../interfaces/web-bluetooth-scanning.idl | 67 ++++ test/wpt/tests/interfaces/webaudio.idl | 9 + test/wpt/tests/interfaces/webauthn.idl | 53 ++- .../webcodecs-hevc-codec-registration.idl | 8 + test/wpt/tests/interfaces/webcodecs.idl | 23 +- test/wpt/tests/interfaces/webgl1.idl | 4 +- test/wpt/tests/interfaces/webgl2.idl | 76 ++-- test/wpt/tests/interfaces/webgpu.idl | 36 +- test/wpt/tests/interfaces/webhid.idl | 2 +- test/wpt/tests/interfaces/webidl.idl | 1 + test/wpt/tests/interfaces/webmidi.idl | 4 +- test/wpt/tests/interfaces/webnn.idl | 55 +-- .../interfaces/webrtc-encoded-transform.idl | 10 +- test/wpt/tests/interfaces/webrtc-stats.idl | 5 +- test/wpt/tests/interfaces/webrtc.idl | 6 +- test/wpt/tests/interfaces/webtransport.idl | 28 +- test/wpt/tests/interfaces/webusb.idl | 9 + .../interfaces/webxr-plane-detection.idl | 32 ++ test/wpt/tests/interfaces/webxr.idl | 3 +- test/wpt/tests/lint.ignore | 65 +++- test/wpt/tests/resources/check-layout-th.js | 3 +- .../tests/resources/chromium/fake-serial.js | 6 +- .../chromium/generic_sensor_mocks.js | 14 +- .../tests/resources/chromium/webxr-test.js | 21 +- .../test/tests/functional/step_wait.html | 22 ++ .../resources/test/tests/unit/late-test.html | 14 +- test/wpt/tests/resources/testdriver.js | 243 ++++++++++++- test/wpt/tests/resources/testharness.js | 31 +- .../partitioned-cookies.tentative.https.html | 93 ++--- .../resources/fetch-access-control.py | 4 +- .../partitioned-cookies-3p-frame.html | 77 ++-- .../resources/partitioned-cookies-3p-sw.js | 56 +-- .../partitioned-cookies-test-helpers.js | 31 ++ .../resources/scope1/redirect.py | 6 +- .../worker_interception_redirect_webworker.py | 6 +- .../worker_interception_redirect_webworker.py | 6 +- .../tentative/static-router/README.md | 2 +- .../resources/or-test/direct1.text | 1 + .../resources/or-test/direct1.text.headers | 1 + .../resources/or-test/direct2.text | 1 + .../resources/or-test/direct2.text.headers | 1 + .../static-router/resources/router-rules.js | 30 ++ .../static-router/resources/simple.csv | 1 + .../resources/static-router-sw.js | 19 +- .../static-router-main-resource.https.html | 24 +- .../static-router-subresource.https.html | 125 ++++++- test/wpt/tests/storage/buckets/META.yml | 8 +- ...ket-quota-indexeddb.tentative.https.any.js | 11 +- .../storage/estimate-indexeddb.https.any.js | 17 +- .../storagemanager-estimate.https.any.js | 68 +--- test/wpt/tests/websockets/META.yml | 1 - .../websockets/handlers/sleep_10_v13_wsh.py | 11 +- .../close/close-connecting-async.any.js | 31 ++ .../websockets/opening-handshake/006.html | 58 +++ test/wpt/tests/xhr/resources/auth1/auth.py | 5 +- test/wpt/tests/xhr/resources/auth10/auth.py | 6 +- test/wpt/tests/xhr/resources/auth11/auth.py | 5 +- test/wpt/tests/xhr/resources/auth2/auth.py | 6 +- .../tests/xhr/resources/auth2/corsenabled.py | 5 +- test/wpt/tests/xhr/resources/auth3/auth.py | 5 +- test/wpt/tests/xhr/resources/auth4/auth.py | 5 +- .../tests/xhr/resources/auth7/corsenabled.py | 5 +- .../auth8/corsenabled-no-authorize.py | 5 +- test/wpt/tests/xhr/resources/auth9/auth.py | 5 +- .../xhr/responsexml-document-properties.htm | 7 +- test/wpt/tests/xhr/send-redirect.htm | 33 +- 218 files changed, 4367 insertions(+), 1489 deletions(-) create mode 100644 test/wpt/tests/common/dummy.json create mode 100644 test/wpt/tests/common/top-layer.js create mode 100644 test/wpt/tests/fetch/api/basic/request-private-network-headers.tentative.any.js create mode 100644 test/wpt/tests/fetch/api/basic/url-parsing.sub.html create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-keepalive.https.any.js create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.css create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.json create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/import-declaration-type-css.js create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/import-declaration-type-json.js create mode 100644 test/wpt/tests/fetch/api/response/response-headers-guard.any.js create mode 100644 test/wpt/tests/fetch/content-encoding/big-gzip-body.https.any.js create mode 100644 test/wpt/tests/fetch/content-encoding/resources/big.text.gz create mode 100644 test/wpt/tests/fetch/content-encoding/resources/big.text.gz.headers create mode 100644 test/wpt/tests/fetch/fetch-later/activate-after.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/basic.tentative.https.worker.js create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/iframe.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/new-window.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/quota.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/resources/fetch-later.html create mode 100644 test/wpt/tests/fetch/fetch-later/resources/header-referrer-helper.js create mode 100644 test/wpt/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js rename test/wpt/tests/fetch/fetch-later/{sendondiscard.tentative.https.window.js => send-on-discard/send-multiple.tentative.https.window.js} (69%) delete mode 100644 test/wpt/tests/fetch/metadata/portal.https.sub.html rename test/wpt/tests/fetch/private-network-access/resources/{fenced-frame-local-network-access-target.https.html => fenced-frame-private-network-access-target.https.html} (100%) rename test/wpt/tests/fetch/private-network-access/resources/{fenced-frame-local-network-access.https.html => fenced-frame-private-network-access.https.html} (100%) rename test/wpt/tests/fetch/private-network-access/resources/{fenced-frame-local-network-access.https.html.headers => fenced-frame-private-network-access.https.html.headers} (100%) create mode 100644 test/wpt/tests/fetch/private-network-access/resources/opener.html create mode 100644 test/wpt/tests/fetch/private-network-access/window-open.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/window-open.tentative.window.js create mode 100644 test/wpt/tests/interfaces/audio-session.idl create mode 100644 test/wpt/tests/interfaces/captured-mouse-events.idl delete mode 100644 test/wpt/tests/interfaces/close-watcher.idl delete mode 100644 test/wpt/tests/interfaces/css-nesting.idl delete mode 100644 test/wpt/tests/interfaces/css-toggle.tentative.idl create mode 100644 test/wpt/tests/interfaces/css-view-transitions-2.idl create mode 100644 test/wpt/tests/interfaces/element-capture.idl delete mode 100644 test/wpt/tests/interfaces/gpc-spec.idl create mode 100644 test/wpt/tests/interfaces/invokers.tentative.idl create mode 100644 test/wpt/tests/interfaces/managed-configuration.idl create mode 100644 test/wpt/tests/interfaces/private-network-access.idl delete mode 100644 test/wpt/tests/interfaces/storage-buckets.tentative.idl delete mode 100644 test/wpt/tests/interfaces/sub-apps.tentative.idl create mode 100644 test/wpt/tests/interfaces/web-bluetooth-scanning.idl create mode 100644 test/wpt/tests/interfaces/webxr-plane-detection.idl create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-test-helpers.js create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct1.text.headers create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/or-test/direct2.text.headers create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/router-rules.js create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.csv create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-connecting-async.any.js create mode 100644 test/wpt/tests/websockets/opening-handshake/006.html diff --git a/lib/core/util.js b/lib/core/util.js index 8d5450ba0c0..c9524b32a80 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -365,6 +365,7 @@ async function * convertIterableToBuffer (iterable) { } } +/** @type {globalThis['ReadableStream']} */ let ReadableStream function ReadableStreamFrom (iterable) { if (!ReadableStream) { @@ -395,9 +396,9 @@ function ReadableStreamFrom (iterable) { }, async cancel (reason) { await iterator.return() - } - }, - 0 + }, + type: 'bytes' + } ) } diff --git a/lib/fetch/body.js b/lib/fetch/body.js index fd8481b796d..58da2941a85 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -47,16 +47,19 @@ function extractBody (object, keepalive = false) { stream = object.stream() } else { // 4. Otherwise, set stream to a new ReadableStream object, and set - // up stream. + // up stream with byte reading support. stream = new ReadableStream({ async pull (controller) { - controller.enqueue( - typeof source === 'string' ? textEncoder.encode(source) : source - ) + const buffer = typeof source === 'string' ? textEncoder.encode(source) : source + + if (buffer.byteLength) { + controller.enqueue(buffer) + } + queueMicrotask(() => readableStreamClose(controller)) }, start () {}, - type: undefined + type: 'bytes' }) } @@ -223,13 +226,17 @@ function extractBody (object, keepalive = false) { // When running action is done, close stream. queueMicrotask(() => { controller.close() + controller.byobRequest?.respond(0) }) } else { // Whenever one or more bytes are available and stream is not errored, // enqueue a Uint8Array wrapping an ArrayBuffer containing the available // bytes into stream. if (!isErrored(stream)) { - controller.enqueue(new Uint8Array(value)) + const buffer = new Uint8Array(value) + if (buffer.byteLength) { + controller.enqueue(buffer) + } } } return controller.desiredSize > 0 @@ -237,7 +244,7 @@ function extractBody (object, keepalive = false) { async cancel (reason) { await iterator.return() }, - type: undefined + type: 'bytes' }) } diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index 7b6a606106d..5c1a1760dd0 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -126,7 +126,13 @@ function URLSerializer (url, excludeFragment = false) { const href = url.href const hashLength = url.hash.length - return hashLength === 0 ? href : href.substring(0, href.length - hashLength) + const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength) + + if (!hashLength && href.endsWith('#')) { + return serialized.slice(0, -1) + } + + return serialized } // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 17c3d87ea62..8495ecabde8 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -40,11 +40,13 @@ const { isomorphicEncode, urlIsLocal, urlIsHttpHttpsScheme, - urlHasHttpsScheme + urlHasHttpsScheme, + simpleRangeHeaderValue, + buildContentRange } = require('./util') const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const assert = require('assert') -const { safelyExtractBody } = require('./body') +const { safelyExtractBody, extractBody } = require('./body') const { redirectStatusSet, nullBodyStatus, @@ -57,7 +59,7 @@ const { kHeadersList } = require('../core/symbols') const EE = require('events') const { Readable, pipeline } = require('stream') const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util') -const { dataURLProcessor, serializeAMimeType } = require('./dataURL') +const { dataURLProcessor, serializeAMimeType, parseMIMEType } = require('./dataURL') const { TransformStream } = require('stream/web') const { getGlobalDispatcher } = require('../global') const { webidl } = require('./webidl') @@ -815,38 +817,118 @@ function schemeFetch (fetchParams) { return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) } - const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) + const blob = resolveObjectURL(blobURLEntry.toString()) // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s // object is not a Blob object, then return a network error. - if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { + if (request.method !== 'GET' || !isBlobLike(blob)) { return Promise.resolve(makeNetworkError('invalid method')) } - // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. - const bodyWithType = safelyExtractBody(blobURLEntryObject) + // 3. Let blob be blobURLEntry’s object. + // Note: done above - // 4. Let body be bodyWithType’s body. - const body = bodyWithType[0] + // 4. Let response be a new response. + const response = makeResponse() - // 5. Let length be body’s length, serialized and isomorphic encoded. - const length = isomorphicEncode(`${body.length}`) + // 5. Let fullLength be blob’s size. + const fullLength = blob.size - // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence. - const type = bodyWithType[1] ?? '' + // 6. Let serializedFullLength be fullLength, serialized and isomorphic encoded. + const serializedFullLength = isomorphicEncode(`${fullLength}`) - // 7. Return a new response whose status message is `OK`, header list is - // « (`Content-Length`, length), (`Content-Type`, type) », and body is body. - const response = makeResponse({ - statusText: 'OK', - headersList: [ - ['content-length', { name: 'Content-Length', value: length }], - ['content-type', { name: 'Content-Type', value: type }] - ] - }) + // 7. Let type be blob’s type. + const type = blob.type + + // 8. If request’s header list does not contain `Range`: + // 9. Otherwise: + if (!request.headersList.contains('range')) { + // 1. Let bodyWithType be the result of safely extracting blob. + // Note: in the FileAPI a blob "object" is a Blob *or* a MediaSource. + // In node, this can only ever be a Blob. Therefore we can safely + // use extractBody directly. + const bodyWithType = extractBody(blob) + + // 2. Set response’s status message to `OK`. + response.statusText = 'OK' + + // 3. Set response’s body to bodyWithType’s body. + response.body = bodyWithType[0] + + // 4. Set response’s header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ». + response.headersList.set('content-length', serializedFullLength) + response.headersList.set('content-type', type) + } else { + // 1. Set response’s range-requested flag. + response.rangeRequested = true + + // 2. Let rangeHeader be the result of getting `Range` from request’s header list. + const rangeHeader = request.headersList.get('range') + + // 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true. + const rangeValue = simpleRangeHeaderValue(rangeHeader, true) + + // 4. If rangeValue is failure, then return a network error. + if (rangeValue === 'failure') { + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + } + + // 5. Let (rangeStart, rangeEnd) be rangeValue. + let { rangeStartValue: rangeStart, rangeEndValue: rangeEnd } = rangeValue + + // 6. If rangeStart is null: + // 7. Otherwise: + if (rangeStart === null) { + // 1. Set rangeStart to fullLength − rangeEnd. + rangeStart = fullLength - rangeEnd + + // 2. Set rangeEnd to rangeStart + rangeEnd − 1. + rangeEnd = rangeStart + rangeEnd - 1 + } else { + // 1. If rangeStart is greater than or equal to fullLength, then return a network error. + if (rangeStart >= fullLength) { + return Promise.resolve(makeNetworkError('Range start is greater than the blob\'s size.')) + } + + // 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set + // rangeEnd to fullLength − 1. + if (rangeEnd === null || rangeEnd >= fullLength) { + rangeEnd = fullLength - 1 + } + } + + // 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart, + // rangeEnd + 1, and type. + const slicedBlob = blob.slice(rangeStart, rangeEnd, type) - response.body = body + // 9. Let slicedBodyWithType be the result of safely extracting slicedBlob. + // Note: same reason as mentioned above as to why we use extractBody + const slicedBodyWithType = extractBody(slicedBlob) + // 10. Set response’s body to slicedBodyWithType’s body. + response.body = slicedBodyWithType[0] + + // 11. Let serializedSlicedLength be slicedBlob’s size, serialized and isomorphic encoded. + const serializedSlicedLength = isomorphicEncode(`${slicedBlob.size}`) + + // 12. Let contentRange be the result of invoking build a content range given rangeStart, + // rangeEnd, and fullLength. + const contentRange = buildContentRange(rangeStart, rangeEnd, fullLength) + + // 13. Set response’s status to 206. + response.status = 206 + + // 14. Set response’s status message to `Partial Content`. + response.statusText = 'Partial Content' + + // 15. Set response’s header list to « (`Content-Length`, serializedSlicedLength), + // (`Content-Type`, type), (`Content-Range`, contentRange) ». + response.headersList.set('content-length', serializedSlicedLength) + response.headersList.set('content-type', type) + response.headersList.set('content-range', contentRange) + } + + // 10. Return response. return Promise.resolve(response) } case 'data:': { @@ -908,92 +990,147 @@ function finalizeResponse (fetchParams, response) { // https://fetch.spec.whatwg.org/#fetch-finale function fetchFinale (fetchParams, response) { - // 1. If response is a network error, then: - if (response.type === 'error') { - // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». - response.urlList = [fetchParams.request.urlList[0]] - - // 2. Set response’s timing info to the result of creating an opaque timing - // info for fetchParams’s timing info. - response.timingInfo = createOpaqueTimingInfo({ - startTime: fetchParams.timingInfo.startTime - }) - } + // 1. Let timingInfo be fetchParams’s timing info. + let timingInfo = fetchParams.timingInfo + + // 2. If response is not a network error and fetchParams’s request’s client is a secure context, + // then set timingInfo’s server-timing headers to the result of getting, decoding, and splitting + // `Server-Timing` from response’s internal response’s header list. + // TODO - // 2. Let processResponseEndOfBody be the following steps: + // 3. Let processResponseEndOfBody be the following steps: const processResponseEndOfBody = () => { - // 1. Set fetchParams’s request’s done flag. - fetchParams.request.done = true - - // If fetchParams’s process response end-of-body is not null, - // then queue a fetch task to run fetchParams’s process response - // end-of-body given response with fetchParams’s task destination. - if (fetchParams.processResponseEndOfBody != null) { - queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + // 1. Let unsafeEndTime be the unsafe shared current time. + const unsafeEndTime = Date.now() // ? + + // 2. If fetchParams’s request’s destination is "document", then set fetchParams’s controller’s + // full timing info to fetchParams’s timing info. + if (fetchParams.request.destination === 'document') { + fetchParams.controller.fullTimingInfo = timingInfo + } + + // 3. Set fetchParams’s controller’s report timing steps to the following steps given a global object global: + fetchParams.controller.reportTimingSteps = () => { + // 1. If fetchParams’s request’s URL’s scheme is not an HTTP(S) scheme, then return. + if (fetchParams.request.url.protocol !== 'https:') { + return + } + + // 2. Set timingInfo’s end time to the relative high resolution time given unsafeEndTime and global. + timingInfo.endTime = unsafeEndTime + + // 3. Let cacheState be response’s cache state. + let cacheState = response.cacheState + + // 4. Let bodyInfo be response’s body info. + const bodyInfo = response.bodyInfo + + // 5. If response’s timing allow passed flag is not set, then set timingInfo to the result of creating an + // opaque timing info for timingInfo and set cacheState to the empty string. + if (!response.timingAllowPassed) { + timingInfo = createOpaqueTimingInfo(timingInfo) + + cacheState = '' + } + + // 6. Let responseStatus be 0. + let responseStatus = 0 + + // 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false: + if (fetchParams.request.mode !== 'navigator' || !response.hasCrossOriginRedirects) { + // 1. Set responseStatus to response’s status. + responseStatus = response.status + + // 2. Let mimeType be the result of extracting a MIME type from response’s header list. + const mimeType = parseMIMEType(response.headersList.get('content-type')) // TODO: fix + + // 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType. + if (mimeType !== 'failure') { + // TODO + } + } + + // 8. If fetchParams’s request’s initiator type is non-null, then mark resource timing given timingInfo, + // fetchParams’s request’s URL, fetchParams’s request’s initiator type, global, cacheState, bodyInfo, + // and responseStatus. + if (fetchParams.request.initiatorType != null) { + // TODO: update markresourcetiming + markResourceTiming(timingInfo, fetchParams.request.url, fetchParams.request.initiatorType, globalThis, cacheState, bodyInfo, responseStatus) + } } + + // 4. Let processResponseEndOfBodyTask be the following steps: + const processResponseEndOfBodyTask = () => { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // 2. If fetchParams’s process response end-of-body is non-null, then run fetchParams’s process + // response end-of-body given response. + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + } + + // 3. If fetchParams’s request’s initiator type is non-null and fetchParams’s request’s client’s + // global object is fetchParams’s task destination, then run fetchParams’s controller’s report + // timing steps given fetchParams’s request’s client’s global object. + if (fetchParams.request.initiatorType != null) { + fetchParams.controller.reportTimingSteps() + } + } + + // 5. Queue a fetch task to run processResponseEndOfBodyTask with fetchParams’s task destination + queueMicrotask(() => processResponseEndOfBodyTask()) } - // 3. If fetchParams’s process response is non-null, then queue a fetch task - // to run fetchParams’s process response given response, with fetchParams’s - // task destination. + // 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s + // process response given response, with fetchParams’s task destination. if (fetchParams.processResponse != null) { queueMicrotask(() => fetchParams.processResponse(response)) } - // 4. If response’s body is null, then run processResponseEndOfBody. - if (response.body == null) { + // 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response. + const internalResponse = response.type === 'error' ? response : (response.internalResponse ?? response) + + // 6. If internalResponse’s body is null, then run processResponseEndOfBody. + // 7. Otherwise: + if (internalResponse.body == null) { processResponseEndOfBody() } else { - // 5. Otherwise: - - // 1. Let transformStream be a new a TransformStream. - - // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, - // enqueues chunk in transformStream. - const identityTransformAlgorithm = (chunk, controller) => { - controller.enqueue(chunk) - } - - // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm - // and flushAlgorithm set to processResponseEndOfBody. + // 1. Let transformStream be a new TransformStream. + // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, enqueues chunk in transformStream. + // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm and flushAlgorithm + // set to processResponseEndOfBody. const transformStream = new TransformStream({ start () {}, - transform: identityTransformAlgorithm, + transform (chunk, controller) { + controller.enqueue(chunk) + }, flush: processResponseEndOfBody - }, { - size () { - return 1 - } - }, { - size () { - return 1 - } }) - // 4. Set response’s body to the result of piping response’s body through transformStream. - response.body = { stream: response.body.stream.pipeThrough(transformStream) } - } + // 4. Set internalResponse’s body’s stream to the result of internalResponse’s body’s stream piped through transformStream. + internalResponse.body.stream.pipeThrough(transformStream) - // 6. If fetchParams’s process response consume body is non-null, then: - if (fetchParams.processResponseConsumeBody != null) { - // 1. Let processBody given nullOrBytes be this step: run fetchParams’s - // process response consume body given response and nullOrBytes. - const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes) + const byteStream = new ReadableStream({ + readableStream: transformStream.readable, + async start (controller) { + const reader = this.readableStream.getReader() - // 2. Let processBodyError be this step: run fetchParams’s process - // response consume body given response and failure. - const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure) + while (true) { + const { done, value } = await reader.read() - // 3. If response’s body is null, then queue a fetch task to run processBody - // given null, with fetchParams’s task destination. - if (response.body == null) { - queueMicrotask(() => processBody(null)) - } else { - // 4. Otherwise, fully read response’s body given processBody, processBodyError, - // and fetchParams’s task destination. - return fullyReadBody(response.body, processBody, processBodyError) - } - return Promise.resolve() + if (done) { + queueMicrotask(() => readableStreamClose(controller)) + break + } + + controller.enqueue(value) + } + }, + type: 'bytes' + }) + + internalResponse.body.stream = byteStream } } @@ -1795,9 +1932,8 @@ async function httpNetworkFetch ( // TODO // 15. Let stream be a new ReadableStream. - // 16. Set up stream with pullAlgorithm set to pullAlgorithm, - // cancelAlgorithm set to cancelAlgorithm, highWaterMark set to - // highWaterMark, and sizeAlgorithm set to sizeAlgorithm. + // 16. Set up stream with byte reading support with pullAlgorithm set to pullAlgorithm, + // cancelAlgorithm set to cancelAlgorithm. if (!ReadableStream) { ReadableStream = require('stream/web').ReadableStream } @@ -1812,13 +1948,8 @@ async function httpNetworkFetch ( }, async cancel (reason) { await cancelAlgorithm(reason) - } - }, - { - highWaterMark: 0, - size () { - return 1 - } + }, + type: 'bytes' } ) @@ -1898,7 +2029,10 @@ async function httpNetworkFetch ( // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes // into stream. - fetchParams.controller.controller.enqueue(new Uint8Array(bytes)) + const buffer = new Uint8Array(bytes) + if (buffer.byteLength) { + fetchParams.controller.controller.enqueue(buffer) + } // 8. If stream is errored, then terminate the ongoing fetch. if (isErrored(stream)) { diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 73386123e33..c97951065be 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -335,10 +335,10 @@ function makeResponse (init) { cacheState: '', statusText: '', ...init, - headersList: init.headersList - ? new HeadersList(init.headersList) + headersList: init?.headersList + ? new HeadersList(init?.headersList) : new HeadersList(), - urlList: init.urlList ? [...init.urlList] : [] + urlList: init?.urlList ? [...init.urlList] : [] } } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index b12142c7f42..e0426be3bcd 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -928,9 +928,10 @@ function isomorphicDecode (input) { function readableStreamClose (controller) { try { controller.close() + controller.byobRequest?.respond(0) } catch (err) { // TODO: add comment explaining why this error occurs. - if (!err.message.includes('Controller is already closed')) { + if (!err.message.includes('Controller is already closed') && !err.message.includes('ReadableStream is already closed')) { throw err } } @@ -1018,6 +1019,173 @@ function urlIsHttpHttpsScheme (url) { return protocol === 'http:' || protocol === 'https:' } +/** @type {import('./dataURL')['collectASequenceOfCodePoints']} */ +let collectASequenceOfCodePoints + +/** + * @see https://fetch.spec.whatwg.org/#simple-range-header-value + * @param {string} value + * @param {boolean} allowWhitespace + */ +function simpleRangeHeaderValue (value, allowWhitespace) { + // Note: avoid circular require + collectASequenceOfCodePoints ??= require('./dataURL').collectASequenceOfCodePoints + + // 1. Let data be the isomorphic decoding of value. + // Note: isomorphic decoding takes a sequence of bytes (ie. a Uint8Array) and turns it into a string, + // nothing more. We obviously don't need to do that if value is a string already. + const data = value + + // 2. If data does not start with "bytes", then return failure. + if (!data.startsWith('bytes')) { + return 'failure' + } + + // 3. Let position be a position variable for data, initially pointing at the 5th code point of data. + const position = { position: 5 } + + // 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, + // from data given position. + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === '\t' || char === ' ', + data, + position + ) + } + + // 5. If the code point at position within data is not U+003D (=), then return failure. + if (data.charCodeAt(position.position) !== 0x3D) { + return 'failure' + } + + // 6. Advance position by 1. + position.position++ + + // 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from + // data given position. + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === '\t' || char === ' ', + data, + position + ) + } + + // 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits, + // from data given position. + const rangeStart = collectASequenceOfCodePoints( + (char) => { + const code = char.charCodeAt(0) + + return code >= 0x30 && code <= 0x39 + }, + data, + position + ) + + // 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the + // empty string; otherwise null. + const rangeStartValue = rangeStart.length ? Number(rangeStart) : null + + // 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, + // from data given position. + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === '\t' || char === ' ', + data, + position + ) + } + + // 11. If the code point at position within data is not U+002D (-), then return failure. + if (data.charCodeAt(position.position) !== 0x2D) { + return 'failure' + } + + // 12. Advance position by 1. + position.position++ + + // 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab + // or space, from data given position. + // Note from Khafra: its the same fucking step again lol + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === '\t' || char === ' ', + data, + position + ) + } + + // 14. Let rangeEnd be the result of collecting a sequence of code points that are + // ASCII digits, from data given position. + // Note from Khafra: you wouldn't guess it, but this is also the same step as #8 + const rangeEnd = collectASequenceOfCodePoints( + (char) => { + const code = char.charCodeAt(0) + + return code >= 0x30 && code <= 0x39 + }, + data, + position + ) + + // 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd + // is not the empty string; otherwise null. + // Note from Khafra: THE SAME STEP, AGAIN!!! + // Note: why interpret as a decimal if we only collect ascii digits? + const rangeEndValue = rangeEnd.length ? Number(rangeEnd) : null + + // 16. If position is not past the end of data, then return failure. + if (position.position < data.length) { + return 'failure' + } + + // 17. If rangeEndValue and rangeStartValue are null, then return failure. + if (rangeEndValue === null && rangeStartValue === null) { + return 'failure' + } + + // 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is + // greater than rangeEndValue, then return failure. + // Note: ... when can they not be numbers? + if (rangeStartValue > rangeEndValue) { + return 'failure' + } + + // 19. Return (rangeStartValue, rangeEndValue). + return { rangeStartValue, rangeEndValue } +} + +/** + * @see https://fetch.spec.whatwg.org/#build-a-content-range + * @param {number} rangeStart + * @param {number} rangeEnd + * @param {number} fullLength + */ +function buildContentRange (rangeStart, rangeEnd, fullLength) { + // 1. Let contentRange be `bytes `. + let contentRange = 'bytes ' + + // 2. Append rangeStart, serialized and isomorphic encoded, to contentRange. + contentRange += isomorphicEncode(`${rangeStart}`) + + // 3. Append 0x2D (-) to contentRange. + contentRange += '-' + + // 4. Append rangeEnd, serialized and isomorphic encoded to contentRange. + contentRange += isomorphicEncode(`${rangeEnd}`) + + // 5. Append 0x2F (/) to contentRange. + contentRange += '/' + + // 6. Append fullLength, serialized and isomorphic encoded to contentRange. + contentRange += isomorphicEncode(`${fullLength}`) + + // 7. Return contentRange. + return contentRange +} + /** * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. */ @@ -1067,5 +1235,7 @@ module.exports = { urlHasHttpsScheme, urlIsHttpHttpsScheme, readAllBytes, - normalizeMethodRecord + normalizeMethodRecord, + simpleRangeHeaderValue, + buildContentRange } diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js index e0fa69726b4..4bc60b3b90b 100644 --- a/lib/websocket/connection.js +++ b/lib/websocket/connection.js @@ -261,6 +261,7 @@ function onSocketClose () { // attribute initialized to the result of applying UTF-8 // decode without BOM to the WebSocket connection close // reason. + // TODO: process.nextTick fireEvent('close', ws, CloseEvent, { wasClean, code, reason }) diff --git a/lib/websocket/util.js b/lib/websocket/util.js index 6c59b2c2380..d15a63cde92 100644 --- a/lib/websocket/util.js +++ b/lib/websocket/util.js @@ -182,6 +182,7 @@ function failWebsocketConnection (ws, reason) { } if (reason) { + // TODO: process.nextTick fireEvent('error', ws, ErrorEvent, { error: new Error(reason) }) diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 82b9080f407..d12b4936978 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -36,6 +36,7 @@ const server = createServer(async (req, res) => { res.setHeader('content-type', 'text/html') // fall through } + case '/fetch/content-encoding/resources/big.text.gz': case '/service-workers/cache-storage/resources/simple.txt': case '/fetch/content-encoding/resources/foo.octetstream.gz': case '/fetch/content-encoding/resources/foo.text.gz': diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 5910bf37f6f..34f91ed8375 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -71,6 +71,10 @@ "Fetch with TacO and mode \"cors\" needs an Origin header" ] }, + "request-private-network-headers.tentative.any.js": { + "note": "undici doesn't filter headers", + "skip": true + }, "request-referrer.any.js": { "note": "TODO(@KhafraDev): url referrer test could probably be fixed", "fail": [ @@ -228,6 +232,10 @@ "note": "document is not defined", "skip": true }, + "redirect-keepalive.https.any.js": { + "note": "document is not defined", + "skip": true + }, "redirect-location-escape.tentative.any.js": { "note": "TODO(@KhafraDev): crashes runner", "skip": true diff --git a/test/wpt/status/websockets.status.json b/test/wpt/status/websockets.status.json index 68bc6e2ebe7..9f5a6308a1d 100644 --- a/test/wpt/status/websockets.status.json +++ b/test/wpt/status/websockets.status.json @@ -4,6 +4,18 @@ "skip": true } }, + "interfaces": { + "WebSocket": { + "close": { + "close-connecting-async.any.js": { + "note": "TODO - need to add route for handshake delay", + "fail": [ + "close event should be fired asynchronously when WebSocket is connecting" + ] + } + } + } + }, "Create-blocked-port.any.js": { "note": "TODO(@KhafraDev): investigate failure", "fail": [ diff --git a/test/wpt/tests/.azure-pipelines.yml b/test/wpt/tests/.azure-pipelines.yml index 75a87df90f0..1a21d2f7a00 100644 --- a/test/wpt/tests/.azure-pipelines.yml +++ b/test/wpt/tests/.azure-pipelines.yml @@ -100,9 +100,6 @@ jobs: inputs: versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_fonts.yml - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/color_profile.yml @@ -144,8 +141,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - # TODO(#40525): Revert back to 3.7 once the Mac agent's Python v3.7 contains bz2 again. - versionSpec: '3.7.16' + versionSpec: '3.7' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: @@ -177,8 +173,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - # TODO(#40525): Revert back to 3.7 once the Mac agent's Python v3.7 contains bz2 again. - versionSpec: '3.7.16' + versionSpec: '3.7' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: @@ -211,8 +206,7 @@ jobs: # full checkout required - task: UsePythonVersion@0 inputs: - # TODO(#40525): Revert back to 3.7 once the Mac agent's Python v3.7 contains bz2 again. - versionSpec: '3.7.16' + versionSpec: '3.7' - template: tools/ci/azure/install_chrome.yml - template: tools/ci/azure/install_firefox.yml - template: tools/ci/azure/update_hosts.yml @@ -373,9 +367,6 @@ jobs: versionSpec: '3.11' - template: tools/ci/azure/system_info.yml - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/install_edge.yml parameters: @@ -412,9 +403,6 @@ jobs: versionSpec: '3.11' - template: tools/ci/azure/system_info.yml - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/install_edge.yml parameters: @@ -450,9 +438,6 @@ jobs: inputs: versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/install_edge.yml parameters: @@ -488,9 +473,6 @@ jobs: inputs: versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/install_safari.yml @@ -531,9 +513,6 @@ jobs: inputs: versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/install_safari.yml @@ -571,9 +550,6 @@ jobs: inputs: versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - - template: tools/ci/azure/pip_install.yml - parameters: - packages: virtualenv - template: tools/ci/azure/install_certs.yml - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/update_hosts.yml diff --git a/test/wpt/tests/.taskcluster.yml b/test/wpt/tests/.taskcluster.yml index c817999b8e6..2a005df772f 100644 --- a/test/wpt/tests/.taskcluster.yml +++ b/test/wpt/tests/.taskcluster.yml @@ -7,7 +7,7 @@ tasks: run_task: $if: 'tasks_for == "github-push"' then: - $if: 'event.ref in ["refs/heads/master", "refs/heads/epochs/daily", "refs/heads/epochs/weekly", "refs/heads/triggers/chrome_stable", "refs/heads/triggers/chrome_beta", "refs/heads/triggers/chrome_dev", "refs/heads/triggers/chrome_nightly", "refs/heads/triggers/firefox_stable", "refs/heads/triggers/firefox_beta", "refs/heads/triggers/firefox_nightly", "refs/heads/triggers/webkitgtk_minibrowser_stable", "refs/heads/triggers/webkitgtk_minibrowser_beta", "refs/heads/triggers/webkitgtk_minibrowser_nightly", "refs/heads/triggers/servo_nightly"]' + $if: 'event.ref in ["refs/heads/master", "refs/heads/epochs/daily", "refs/heads/epochs/weekly", "refs/heads/triggers/chrome_stable", "refs/heads/triggers/chrome_beta", "refs/heads/triggers/chrome_dev", "refs/heads/triggers/chrome_nightly", "refs/heads/triggers/firefox_stable", "refs/heads/triggers/firefox_beta", "refs/heads/triggers/firefox_nightly", "refs/heads/triggers/firefox_android_nightly", "refs/heads/triggers/webkitgtk_minibrowser_stable", "refs/heads/triggers/webkitgtk_minibrowser_beta", "refs/heads/triggers/webkitgtk_minibrowser_nightly", "refs/heads/triggers/servo_nightly"]' then: true else: false else: @@ -57,7 +57,7 @@ tasks: owner: ${owner} source: ${event.repository.clone_url} payload: - image: webplatformtests/wpt:0.54 + image: webplatformtests/wpt:0.55 maxRunTime: 7200 artifacts: public/results: diff --git a/test/wpt/tests/FileAPI/META.yml b/test/wpt/tests/FileAPI/META.yml index 506a59fec1e..66227c8224b 100644 --- a/test/wpt/tests/FileAPI/META.yml +++ b/test/wpt/tests/FileAPI/META.yml @@ -1,6 +1,6 @@ spec: https://w3c.github.io/FileAPI/ suggested_reviewers: - inexorabletash - - zqzhang - jdm - mkruisselbrink + - annevk diff --git a/test/wpt/tests/FileAPI/blob/Blob-stream.any.js b/test/wpt/tests/FileAPI/blob/Blob-stream.any.js index 87710a171a9..453144cac96 100644 --- a/test/wpt/tests/FileAPI/blob/Blob-stream.any.js +++ b/test/wpt/tests/FileAPI/blob/Blob-stream.any.js @@ -70,7 +70,18 @@ promise_test(async() => { await garbageCollect(); const chunks = await read_all_chunks(stream, { perform_gc: true }); assert_array_equals(chunks, input_arr); -}, "Blob.stream() garbage collection of blob shouldn't break stream" + +}, "Blob.stream() garbage collection of blob shouldn't break stream " + + "consumption") + +promise_test(async() => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + let blob = new Blob([typed_arr]); + const chunksPromise = read_all_chunks(blob.stream()); + // It somehow matters to do GC here instead of doing `perform_gc: true` + await garbageCollect(); + assert_array_equals(await chunksPromise, input_arr); +}, "Blob.stream() garbage collection of stream shouldn't break stream " + "consumption") promise_test(async () => { diff --git a/test/wpt/tests/common/META.yml b/test/wpt/tests/common/META.yml index ca4d2e523c0..963dff9d1f1 100644 --- a/test/wpt/tests/common/META.yml +++ b/test/wpt/tests/common/META.yml @@ -1,3 +1,2 @@ suggested_reviewers: - - zqzhang - deniak diff --git a/test/wpt/tests/common/dispatcher/dispatcher.js b/test/wpt/tests/common/dispatcher/dispatcher.js index a0f9f43e622..ce17a7c9145 100644 --- a/test/wpt/tests/common/dispatcher/dispatcher.js +++ b/test/wpt/tests/common/dispatcher/dispatcher.js @@ -1,7 +1,26 @@ // Define a universal message passing API. It works cross-origin and across // browsing context groups. const dispatcher_path = "/common/dispatcher/dispatcher.py"; -const dispatcher_url = new URL(dispatcher_path, location.href).href; + +// Finds the nearest ancestor window that has a non srcdoc location. This should +// give us a usable location for constructing further URLs. +function findLocationFromAncestors(w) { + if (w.location.href == 'about:srcdoc') { + return findLocationFromAncestors(w.parent); + } + return w.location; +} + +// Handles differences between workers vs frames (src vs srcdoc). +function findLocation() { + if (location.href == 'about:srcdoc') { + return findLocationFromAncestors(window.parent); + } + return location; +} + +const dispatcherLocation = findLocation(); +const dispatcher_url = new URL(dispatcher_path, dispatcherLocation).href; // Return a promise, limiting the number of concurrent accesses to a shared // resources to |max_concurrent_access|. @@ -138,7 +157,7 @@ const cacheableShowRequestHeaders = function(origin, uuid) { // protocol: (optional) Sets the returned URL's `protocol` property. // } function remoteExecutorUrl(uuid, options) { - const url = new URL("/common/dispatcher/remote-executor.html", location); + const url = new URL("/common/dispatcher/remote-executor.html", dispatcherLocation); url.searchParams.set("uuid", uuid); if (options?.host) { diff --git a/test/wpt/tests/common/dummy.json b/test/wpt/tests/common/dummy.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/test/wpt/tests/common/dummy.json @@ -0,0 +1 @@ +{} diff --git a/test/wpt/tests/common/security-features/subresource/video.py b/test/wpt/tests/common/security-features/subresource/video.py index 7cfbbfa68c8..9db8e9fbb5a 100644 --- a/test/wpt/tests/common/security-features/subresource/video.py +++ b/test/wpt/tests/common/security-features/subresource/video.py @@ -4,7 +4,7 @@ subresource = importlib.import_module("common.security-features.subresource.subresource") def generate_payload(request, server_data): - file = os.path.join(request.doc_root, u"media", u"movie_5.ogv") + file = os.path.join(request.doc_root, u"media", u"movie_5.webm") return open(file, "rb").read() @@ -14,4 +14,4 @@ def main(request, response): response, payload_generator = handler, access_control_allow_origin = b"*", - content_type = b"video/ogg") + content_type = b"video/webm") diff --git a/test/wpt/tests/common/top-layer.js b/test/wpt/tests/common/top-layer.js new file mode 100644 index 00000000000..2dc8ce3893a --- /dev/null +++ b/test/wpt/tests/common/top-layer.js @@ -0,0 +1,29 @@ +// This function is a version of test_driver.bless which works while there are +// elements in the top layer: +// https://github.com/web-platform-tests/wpt/issues/41218. +// Pass it the element at the top of the top layer stack. +window.blessTopLayer = async (topLayerElement) => { + const button = document.createElement('button'); + topLayerElement.append(button); + let wait_click = new Promise(resolve => button.addEventListener("click", resolve, {once: true})); + await test_driver.click(button); + await wait_click; + button.remove(); +}; + +window.isTopLayer = (el) => { + // A bit of a hack. Just test a few properties of the ::backdrop pseudo + // element that change when in the top layer. + const properties = ['right','background']; + const testEl = document.createElement('div'); + document.body.appendChild(testEl); + const computedStyle = getComputedStyle(testEl, '::backdrop'); + const nonTopLayerValues = properties.map(p => computedStyle[p]); + testEl.remove(); + for(let i=0;i + + + + + + + +
+ diff --git a/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js b/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js index f68d90ef9aa..08229f9cfc7 100644 --- a/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js +++ b/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js @@ -75,10 +75,10 @@ keepaliveCorsBasicTest( function keepaliveCorsInUnloadTest(description, origin, method) { const evt = 'unload'; for (const mode of ['no-cors', 'cors']) { - for (const disallowOrigin of [false, true]) { + for (const disallowCrossOrigin of [false, true]) { const desc = `${description} ${method} request in ${evt} [${mode} mode` + - (disallowOrigin ? `, server forbid CORS]` : `]`); - const shouldPass = !disallowOrigin || mode === 'no-cors'; + (disallowCrossOrigin ? ']' : ', server forbid CORS]'); + const expectTokenExist = !disallowCrossOrigin || mode === 'no-cors'; promise_test(async (test) => { const token1 = token(); const iframe = document.createElement('iframe'); @@ -87,14 +87,14 @@ function keepaliveCorsInUnloadTest(description, origin, method) { requestOrigin: origin, sendOn: evt, mode: mode, - disallowOrigin + disallowCrossOrigin }); document.body.appendChild(iframe); await iframeLoaded(iframe); iframe.remove(); assert_equals(await getTokenFromMessage(), token1); - assertStashedTokenAsync(desc, token1, {shouldPass}); + assertStashedTokenAsync(desc, token1, {expectTokenExist}); }, `${desc}; setting up`); } } diff --git a/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js index bcfc444f5a6..beda8bb8e78 100644 --- a/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js +++ b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js @@ -14,66 +14,6 @@ const { HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT } = get_host_info(); -/** - * In an iframe, test to fetch a keepalive URL that involves in redirect to - * another URL. - */ -function keepaliveRedirectTest( - desc, {origin1 = '', origin2 = '', withPreflight = false} = {}) { - desc = `[keepalive] ${desc}`; - 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); - iframe.remove(); - - assertStashedTokenAsync(desc, tokenToStash); - }, `${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, - shouldPass = 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, {shouldPass}); - }, `${desc}; setting up`); -} - -keepaliveRedirectTest(`same-origin redirect`); -keepaliveRedirectTest( - `same-origin redirect + preflight`, {withPreflight: true}); -keepaliveRedirectTest(`cross-origin redirect`, { - origin1: HTTP_REMOTE_ORIGIN, - origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT -}); -keepaliveRedirectTest(`cross-origin redirect + preflight`, { - origin1: HTTP_REMOTE_ORIGIN, - origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, - withPreflight: true -}); keepaliveRedirectInUnloadTest('same-origin redirect'); keepaliveRedirectInUnloadTest( @@ -88,7 +28,9 @@ keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', { withPreflight: true }); keepaliveRedirectInUnloadTest( - 'redirect to file URL', {url2: 'file://tmp/bar.txt', shouldPass: false}); -keepaliveRedirectInUnloadTest( - 'redirect to data URL', - {url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5', shouldPass: false}); + '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/wpt/tests/fetch/api/redirect/redirect-keepalive.https.any.js b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.https.any.js new file mode 100644 index 00000000000..6765ecac6d7 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.https.any.js @@ -0,0 +1,20 @@ +// META: global=window +// META: title=Fetch API: keepalive handling +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// 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/wpt/tests/fetch/api/request/destination/fetch-destination.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html index 0094b0b6fe8..1b6cf169141 100644 --- a/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html @@ -194,6 +194,40 @@ }); }, 'HTMLLinkElement with rel=stylesheet fetches with a "style" Request.destination'); +// Import declaration with `type: "css"` - style destination +promise_test(t => { + return new Promise((resolve, reject) => { + frame.contentWindow.onerror = reject; + + let node = frame.contentWindow.document.createElement("script"); + node.onload = resolve; + node.onerror = reject; + node.src = "import-declaration-type-css.js"; + node.type = "module"; + frame.contentWindow.document.body.appendChild(node); + }).then(() => { + frame.contentWindow.onerror = null; + }); +}, 'Import declaration with `type: "css"` fetches with a "style" Request.destination'); + +// JSON destination +/////////////////// + +// Import declaration with `type: "json"` - json destination +promise_test(t => { + return new Promise((resolve, reject) => { + frame.contentWindow.onerror = reject; + let node = frame.contentWindow.document.createElement("script"); + node.onload = resolve; + node.onerror = reject; + node.src = "import-declaration-type-json.js"; + node.type = "module"; + frame.contentWindow.document.body.appendChild(node); + }).then(() => { + frame.contentWindow.onerror = null; + }); +}, 'Import declaration with `type: "json"` fetches with a "json" Request.destination'); + // Preload tests //////////////// // HTMLLinkElement with rel=preload and as=fetch - empty string destination @@ -232,6 +266,22 @@ }); }, 'HTMLLinkElement with rel=preload and as=style fetches with a "style" Request.destination'); +// HTMLLinkElement with rel=preload and as=json - json destination +promise_test(t => { + return new Promise((resolve, reject) => { + let node = frame.contentWindow.document.createElement("link"); + node.rel = "preload"; + node.as = "json"; + if (node.as != "json") { + resolve(); + } + node.onload = resolve; + node.onerror = reject; + node.href = "dummy.json?t=2&dest=json"; + frame.contentWindow.document.body.appendChild(node); + }); +}, 'HTMLLinkElement with rel=preload and as=json fetches with a "json" Request.destination'); + // HTMLLinkElement with rel=preload and as=script - script destination promise_test(async t => { await new Promise((resolve, reject) => { diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.css b/test/wpt/tests/fetch/api/request/destination/resources/dummy.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.json b/test/wpt/tests/fetch/api/request/destination/resources/dummy.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.json @@ -0,0 +1 @@ +{} diff --git a/test/wpt/tests/fetch/api/request/destination/resources/import-declaration-type-css.js b/test/wpt/tests/fetch/api/request/destination/resources/import-declaration-type-css.js new file mode 100644 index 00000000000..3c8cf1f44b7 --- /dev/null +++ b/test/wpt/tests/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/wpt/tests/fetch/api/request/destination/resources/import-declaration-type-json.js b/test/wpt/tests/fetch/api/request/destination/resources/import-declaration-type-json.js new file mode 100644 index 00000000000..b2d964dd824 --- /dev/null +++ b/test/wpt/tests/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/wpt/tests/fetch/api/request/request-headers.any.js b/test/wpt/tests/fetch/api/request/request-headers.any.js index 22925e01b69..a766bcb5fff 100644 --- a/test/wpt/tests/fetch/api/request/request-headers.any.js +++ b/test/wpt/tests/fetch/api/request/request-headers.any.js @@ -18,7 +18,6 @@ var invalidRequestHeaders = [ ["Accept-Encoding", "KO"], ["Access-Control-Request-Headers", "KO"], ["Access-Control-Request-Method", "KO"], - ["Access-Control-Request-Private-Network", "KO"], ["Connection", "KO"], ["Content-Length", "KO"], ["Cookie", "KO"], diff --git a/test/wpt/tests/fetch/api/resources/keepalive-helper.js b/test/wpt/tests/fetch/api/resources/keepalive-helper.js index ad1d4b2c7c3..f6f511631e5 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-helper.js +++ b/test/wpt/tests/fetch/api/resources/keepalive-helper.js @@ -11,14 +11,14 @@ // `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. -// `disallowOrigin` to ask the iframe to set up a server that forbids CORS -// requests. +// `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', - disallowOrigin = false + disallowCrossOrigin = false } = {}) { const https = location.protocol.startsWith('https'); frameOrigin = frameOrigin === 'DEFAULT' ? @@ -28,7 +28,7 @@ function getKeepAliveIframeUrl(token, method, { `token=${token}&` + `method=${method}&` + `sendOn=${sendOn}&` + - `mode=${mode}&` + (disallowOrigin ? `disallowOrigin=1&` : ``) + + `mode=${mode}&` + (disallowCrossOrigin ? `disallowCrossOrigin=1&` : ``) + `origin=${requestOrigin}`; } @@ -72,28 +72,105 @@ async function queryToken(token) { 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, {shouldPass = true} = {}) { - async_test((test) => { - new Promise((resolve) => test.step_timeout(resolve, 3000)) - .then(() => { +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((result) => { - assert_equals(result, 'on'); - }) - .then(() => { - test.done(); - }) - .catch(test.step_func((e) => { - if (shouldPass) { - assert_unreached(e); + })) + .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 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`); +} diff --git a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html index 335a1f8e318..f9dae5a34ec 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html +++ b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html @@ -4,14 +4,15 @@ + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html new file mode 100644 index 00000000000..75e9ece7ba5 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html @@ -0,0 +1,19 @@ + + + +FetchLater Referrer Header: No Referrer Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html new file mode 100644 index 00000000000..b9f14171ba3 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html @@ -0,0 +1,25 @@ + + + +FetchLater Referrer Header: Origin When Cross Origin Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html new file mode 100644 index 00000000000..ce7abf92039 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html @@ -0,0 +1,23 @@ + + + +FetchLater Referrer Header: Origin Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html new file mode 100644 index 00000000000..264beddc03a --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html @@ -0,0 +1,24 @@ + + + +FetchLater Referrer Header: Same Origin Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html new file mode 100644 index 00000000000..9133f2496fe --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html @@ -0,0 +1,24 @@ + + + +FetchLater Referrer Header: Strict Origin Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html new file mode 100644 index 00000000000..943d70bbc58 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html @@ -0,0 +1,24 @@ + + + +FetchLater Referrer Header: Strict Origin Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html new file mode 100644 index 00000000000..a602e0003a4 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html @@ -0,0 +1,24 @@ + + + +FetchLater Referrer Header: Unsafe Url Policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/iframe.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/iframe.tentative.https.window.js new file mode 100644 index 00000000000..62505bc81d9 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/iframe.tentative.https.window.js @@ -0,0 +1,65 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +const { + HTTPS_ORIGIN, + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +async function loadElement(el) { + const loaded = new Promise(resolve => el.onload = resolve); + document.body.appendChild(el); + await loaded; +} + +// `host` may be cross-origin +async function loadFetchLaterIframe(host, targetUrl) { + const url = `${host}/fetch/fetch-later/resources/fetch-later.html?url=${ + encodeURIComponent(targetUrl)}`; + const iframe = document.createElement('iframe'); + iframe.src = url; + await loadElement(iframe); + return iframe; +} + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads a blank iframe that fires a fetchLater request. + const iframe = document.createElement('iframe'); + iframe.addEventListener('load', () => { + fetchLater(url, {activateAfter: 0}); + }); + await loadElement(iframe); + + // The iframe should have sent the request. + await expectBeacon(uuid, {count: 1}); +}, 'A blank iframe can trigger fetchLater.'); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads a same-origin iframe that fires a fetchLater request. + await loadFetchLaterIframe(HTTPS_ORIGIN, url); + + // The iframe should have sent the request. + await expectBeacon(uuid, {count: 1}); +}, 'A same-origin iframe can trigger fetchLater.'); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads a same-origin iframe that fires a fetchLater request. + await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, url); + + // The iframe should have sent the request. + await expectBeacon(uuid, {count: 1}); +}, 'A cross-origin iframe can trigger fetchLater.'); diff --git a/test/wpt/tests/fetch/fetch-later/new-window.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/new-window.tentative.https.window.js new file mode 100644 index 00000000000..37b38d7f1dc --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/new-window.tentative.https.window.js @@ -0,0 +1,77 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +const { + HTTPS_ORIGIN, + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +function fetchLaterPopupUrl(host, targetUrl) { + return `${host}/fetch/fetch-later/resources/fetch-later.html?url=${ + encodeURIComponent(targetUrl)}`; +} + +for (const target of ['', '_blank']) { + for (const features in ['', 'popup', 'popup,noopener']) { + parallelPromiseTest( + async t => { + const uuid = token(); + const url = + generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + + // Opens a blank popup window that fires a fetchLater request. + const w = window.open( + `javascript: fetchLater("${url}", {activateAfter: 0})`, target, + features); + await new Promise(resolve => w.addEventListener('load', resolve)); + + // The popup should have sent the request. + await expectBeacon(uuid, {count: 1}); + w.close(); + }, + `A blank window[target='${target}'][features='${ + features}'] can trigger fetchLater.`); + + parallelPromiseTest( + async t => { + const uuid = token(); + const popupUrl = + fetchLaterPopupUrl(HTTPS_ORIGIN, generateSetBeaconURL(uuid)); + + // Opens a same-origin popup that fires a fetchLater request. + const w = window.open(popupUrl, target, features); + await new Promise(resolve => w.addEventListener('load', resolve)); + + // The popup should have sent the request. + await expectBeacon(uuid, {count: 1}); + w.close(); + }, + `A same-origin window[target='${target}'][features='${ + features}'] can trigger fetchLater.`); + + parallelPromiseTest( + async t => { + const uuid = token(); + const popupUrl = fetchLaterPopupUrl( + HTTPS_NOTSAMESITE_ORIGIN, generateSetBeaconURL(uuid)); + + // Opens a cross-origin popup that fires a fetchLater request. + const w = window.open(popupUrl, target, features); + // As events from cross-origin window is not accessible, waiting for + // its message instead. + await new Promise( + resolve => window.addEventListener('message', resolve)); + + // The popup should have sent the request. + await expectBeacon(uuid, {count: 1}); + w.close(); + }, + `A cross-origin window[target='${target}'][features='${ + features}'] can trigger fetchLater.`); + } +} diff --git a/test/wpt/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js new file mode 100644 index 00000000000..5aa759c2346 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js @@ -0,0 +1,28 @@ +// META: title=FetchLater: allowed by CSP +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +'use strict'; + +const { + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +// FetchLater requests allowed by Content Security Policy. +// https://w3c.github.io/webappsec-csp/#should-block-request + +const meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', `connect-src 'self' ${HTTPS_NOTSAMESITE_ORIGIN}`); +document.head.appendChild(meta); + +promise_test(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + fetchLater(url, {activateAfter: 0}); + + await expectBeacon(uuid, {count: 1}); + t.done(); +}, 'FetchLater allowed by CSP should succeed'); diff --git a/test/wpt/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js new file mode 100644 index 00000000000..88490950d3a --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js @@ -0,0 +1,33 @@ +// META: title=FetchLater: blocked by CSP +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +'use strict'; + +const { + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +// FetchLater requests blocked by Content Security Policy are rejected. +// https://w3c.github.io/webappsec-csp/#should-block-request + +const meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', 'connect-src \'self\''); +document.head.appendChild(meta); + +promise_test(async t => { + const uuid = token(); + const cspViolationUrl = + generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + fetchLater(cspViolationUrl, {activateAfter: 0}); + + await new Promise( + resolve => window.addEventListener('securitypolicyviolation', e => { + assert_equals(e.violatedDirective, 'connect-src'); + resolve(); + })); + t.done(); +}, 'FetchLater blocked by CSP should reject'); diff --git a/test/wpt/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js new file mode 100644 index 00000000000..db6b4234b97 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js @@ -0,0 +1,35 @@ +// META: title=FetchLater: redirect blocked by CSP +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +// META: timeout=long + +'use strict'; + +const { + HTTPS_NOTSAMESITE_ORIGIN, +} = get_host_info(); + +// FetchLater requests redirect to URL blocked by Content Security Policy. +// https://w3c.github.io/webappsec-csp/#should-block-request + +const meta = document.createElement('meta'); +meta.setAttribute('http-equiv', 'Content-Security-Policy'); +meta.setAttribute('content', 'connect-src \'self\''); +document.head.appendChild(meta); + +promise_test(async t => { + const uuid = token(); + const cspViolationUrl = + generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); + const url = + `/common/redirect.py?location=${encodeURIComponent(cspViolationUrl)}`; + fetchLater(url, {activateAfter: 0}); + + // TODO(crbug.com/1465781): redirect csp check is handled in browser, of which + // result cannot be populated to renderer at this moment. + await expectBeacon(uuid, {count: 0}); + t.done(); +}, 'FetchLater redirect blocked by CSP should reject'); diff --git a/test/wpt/tests/fetch/fetch-later/quota.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/quota.tentative.https.window.js new file mode 100644 index 00000000000..4fc5979374c --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/quota.tentative.https.window.js @@ -0,0 +1,130 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +const kQuotaPerOrigin = 64 * 1024; // 64 kilobytes per spec. +const {ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); + +// Runs a test case that cover a single fetchLater() call with `body` in its +// request payload. The call is not expected to throw any errors. +function fetchLaterPostTest(body, description) { + test(() => { + const controller = new AbortController(); + const result = fetchLater( + '/fetch-later', + {method: 'POST', signal: controller.signal, body: body}); + assert_false(result.activated); + // Release quota taken by the pending request for subsequent tests. + controller.abort(); + }, description); +} + +// Test small payload for each supported data types. +for (const [dataType, skipCharset] of Object.entries( + BeaconDataTypeToSkipCharset)) { + fetchLaterPostTest( + makeBeaconData(generateSequentialData(0, 1024, skipCharset), dataType), + `A fetchLater() call accept small data in POST request of ${dataType}.`); +} + +// Test various size of payloads for the same origin. +for (const dataType in BeaconDataType) { + if (dataType !== BeaconDataType.FormData && + dataType !== BeaconDataType.URLSearchParams) { + // Skips FormData & URLSearchParams, as browser adds extra bytes to them + // in addition to the user-provided content. It is difficult to test a + // request right at the quota limit. + fetchLaterPostTest( + // Generates data that is exactly 64 kilobytes. + makeBeaconData(generatePayload(kQuotaPerOrigin), dataType), + `A single fetchLater() call takes up the per-origin quota for its ` + + `body of ${dataType}.`); + } +} + +// Test empty payload. +for (const dataType in BeaconDataType) { + test( + () => { + assert_throws_js( + TypeError, () => fetchLater('/', {method: 'POST', body: ''})); + }, + `A single fetchLater() call does not accept empty data in POST request ` + + `of ${dataType}.`); +} + +// Test oversized payload. +for (const dataType in BeaconDataType) { + test( + () => { + assert_throws_dom( + 'QuotaExceededError', + () => fetchLater('/fetch-later', { + method: 'POST', + // Generates data that exceeds 64 kilobytes. + body: + makeBeaconData(generatePayload(kQuotaPerOrigin + 1), dataType) + })); + }, + `A single fetchLater() call is not allowed to exceed per-origin quota ` + + `for its body of ${dataType}.`); +} + +// Test accumulated oversized request. +for (const dataType in BeaconDataType) { + test( + () => { + const controller = new AbortController(); + // Makes the 1st call that sends only half of allowed quota. + fetchLater('/fetch-later', { + method: 'POST', + signal: controller.signal, + body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType) + }); + + // Makes the 2nd call that sends half+1 of allowed quota. + assert_throws_dom('QuotaExceededError', () => { + fetchLater('/fetch-later', { + method: 'POST', + signal: controller.signal, + body: makeBeaconData( + generatePayload(kQuotaPerOrigin / 2 + 1), dataType) + }); + }); + // Release quota taken by the pending requests for subsequent tests. + controller.abort(); + }, + `The 2nd fetchLater() call is not allowed to exceed per-origin quota ` + + `for its body of ${dataType}.`); +} + +// Test various size of payloads across different origins. +for (const dataType in BeaconDataType) { + test( + () => { + const controller = new AbortController(); + // Makes the 1st call that sends only half of allowed quota. + fetchLater('/fetch-later', { + method: 'POST', + signal: controller.signal, + body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType) + }); + + // Makes the 2nd call that sends half+1 of allowed quota, but to a + // different origin. + fetchLater(`${HTTPS_NOTSAMESITE_ORIGIN}/fetch-later`, { + method: 'POST', + signal: controller.signal, + body: + makeBeaconData(generatePayload(kQuotaPerOrigin / 2 + 1), dataType) + }); + // Release quota taken by the pending requests for subsequent tests. + controller.abort(); + }, + `The 2nd fetchLater() call to another origin does not exceed per-origin` + + ` quota for its body of ${dataType}.`); +} diff --git a/test/wpt/tests/fetch/fetch-later/resources/fetch-later.html b/test/wpt/tests/fetch/fetch-later/resources/fetch-later.html new file mode 100644 index 00000000000..b569e1a076a --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/resources/fetch-later.html @@ -0,0 +1,14 @@ + + + + + + diff --git a/test/wpt/tests/fetch/fetch-later/resources/header-referrer-helper.js b/test/wpt/tests/fetch/fetch-later/resources/header-referrer-helper.js new file mode 100644 index 00000000000..374097614ae --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/resources/header-referrer-helper.js @@ -0,0 +1,39 @@ +'use strict'; + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +const REFERRER_ORIGIN = self.location.origin + '/'; +const REFERRER_URL = self.location.href; + +function testReferrerHeader(id, host, expectedReferrer) { + const url = `${ + host}/beacon/resources/inspect-header.py?header=referer&cmd=put&id=${id}`; + + promise_test(t => { + fetchLater(url, {activateAfter: 0}); + return pollResult(expectedReferrer, id).then(result => { + assert_equals(result, expectedReferrer, 'Correct referrer header result'); + }); + }, `Test referer header ${host}`); +} + +function pollResult(expectedReferrer, id) { + const checkUrl = + `/beacon/resources/inspect-header.py?header=referer&cmd=get&id=${id}`; + + return new Promise(resolve => { + function checkResult() { + fetch(checkUrl).then(response => { + assert_equals( + response.status, 200, 'Inspect header response\'s status is 200'); + let result = response.headers.get('x-request-referer'); + + if (result != undefined) { + resolve(result); + } else { + step_timeout(checkResult.bind(this), 100); + } + }); + } + checkResult(); + }); +} diff --git a/test/wpt/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js new file mode 100644 index 00000000000..94877e8321a --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js @@ -0,0 +1,185 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +// NOTE: Due to the restriction of WPT runner, the following tests are all run +// with BackgroundSync off, which is different from some browsers, +// e.g. Chrome, default behavior, as the testing infra does not support enabling +// it. + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document enters BFCache. + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // Creates a fetchLater request with default config in remote, which should + // only be sent on page discarded (not on entering BFCache). + await rc1.executeScript(url => { + fetchLater(url); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to let page enter BFCache. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + // Theoretically, the request should still be pending thus 0 request received. + // However, 1 request is sent, as by default the WPT test runner, e.g. + // content_shell in Chromium, does not enable BackgroundSync permission, + // resulting in forcing request sending on every navigation. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() sends on page entering BFCache if BackgroundSync is off.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // When the remote is put into BFCached, creates a fetchLater request w/ + // activateAfter = 0s. It should be sent out immediately. + await rc1.executeScript(url => { + window.addEventListener('pagehide', e => { + if (e.persisted) { + fetchLater(url, {activateAfter: 0}); + } + }); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + // NOTE: In this case, it does not matter if BackgroundSync is on or off. + await expectBeacon(uuid, {count: 1}); +}, `Call fetchLater() when BFCached with activateAfter=0 sends immediately.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document gets discarded + // on navigated away. + const helper = new RemoteContextHelper(); + // Opens a window without BFCache. + const rc1 = await helper.addWindow(); + + // Creates a fetchLater request in remote which should only be sent on + // navigating away. + await rc1.executeScript(url => { + fetchLater(url); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was NOT BFCached. + assert_equals(undefined, await rc1.executeScript(() => { + return window.pageshowEvent; + })); + + // NOTE: In this case, it does not matter if BackgroundSync is on or off. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() sends on navigating away a page w/o BFCache.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + // Sets no option to test the default behavior when a document gets discarded + // on navigated away. + const helper = new RemoteContextHelper(); + // Opens a window without BFCache. + const rc1 = await helper.addWindow(); + + // Creates 2 fetchLater requests in remote, and one of them is aborted + // immediately. The other one should only be sent right on navigating away. + await rc1.executeScript(url => { + const controller = new AbortController(); + fetchLater(url, {signal: controller.signal}); + fetchLater(url); + controller.abort(); + // Add a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was NOT BFCached. + assert_equals(undefined, await rc1.executeScript(() => { + return window.pageshowEvent; + })); + + // NOTE: In this case, it does not matter if BackgroundSync is on or off. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() does not send aborted request on navigating away a page w/o BFCache.`); + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const options = {activateAfter: 60000}; + const helper = new RemoteContextHelper(); + // Opens a window with noopener so that BFCache will work. + const rc1 = await helper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // Creates a fetchLater request in remote which should only be sent on + // navigating away. + await rc1.executeScript((url) => { + // Sets activateAfter = 1m to indicate the request should NOT be sent out + // immediately. + fetchLater(url, {activateAfter: 60000}); + // Adds a pageshow listener to stash the BFCache event. + window.addEventListener('pageshow', e => { + window.pageshowEvent = e; + }); + }, [url]); + // Navigates away to trigger request sending. + const rc2 = await rc1.navigateToNew(); + // Navigates back. + await rc2.historyBack(); + // Verifies the page was BFCached. + assert_true(await rc1.executeScript(() => { + return window.pageshowEvent.persisted; + })); + + // Theoretically, the request should still be pending thus 0 request received. + // However, 1 request is sent, as by default the WPT test runner, e.g. + // content_shell in Chromium, does not enable BackgroundSync permission, + // resulting in forcing request sending on every navigation, even if page is + // put into BFCache. + await expectBeacon(uuid, {count: 1}); +}, `fetchLater() with activateAfter=1m sends on page entering BFCache if BackgroundSync is off.`); diff --git a/test/wpt/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js new file mode 100644 index 00000000000..c49e0bde87b --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js @@ -0,0 +1,25 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + + // Loads an iframe that creates 2 fetchLater requests. One of them is aborted. + const iframe = await loadScriptAsIframe(` + const url = '${url}'; + const controller = new AbortController(); + fetchLater(url, {signal: controller.signal}); + fetchLater(url, {method: 'POST'}); + controller.abort(); + `); + // Delete the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should not send the aborted request. + await expectBeacon(uuid, {count: 1}); +}, 'A discarded document does not send an already aborted fetchLater request.'); diff --git a/test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js new file mode 100644 index 00000000000..03078b2b516 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js @@ -0,0 +1,32 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js +// META: timeout=long + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const numPerMethod = 20; + const total = numPerMethod * 2; + + // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. + const iframe = await loadScriptAsIframe(` + const url = '${url}'; + for (let i = 0; i < ${numPerMethod}; i++) { + // Changing the URL of each request to avoid HTTP Cache issue. + // See crbug.com/1498203#c17. + fetchLater(url + "&method=GET&i=" + i, + {method: 'GET', activateAfter: 10000}); // 10s + fetchLater(url + "&method=POST&i=" + i, + {method: 'POST', activateAfter: 8000}); // 8s + } + `); + // Delete the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should have sent all requests. + await expectBeacon(uuid, {count: total}); +}, 'A discarded document sends all its fetchLater requests, no matter how much their activateAfter timeout remain.'); diff --git a/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js similarity index 69% rename from test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js rename to test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js index 0613d18dffb..25ce98d446e 100644 --- a/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js +++ b/test/wpt/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js @@ -2,6 +2,7 @@ // META: script=/resources/testharnessreport.js // META: script=/common/utils.js // META: script=/pending-beacon/resources/pending_beacon-helper.js +// META: timeout=long 'use strict'; @@ -13,16 +14,17 @@ parallelPromiseTest(async t => { // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. const iframe = await loadScriptAsIframe(` - const url = "${url}"; + const url = '${url}'; for (let i = 0; i < ${numPerMethod}; i++) { - let get = fetchLater(url); - let post = fetchLater(url, {method: 'POST'}); + // Changing the URL of each request to avoid HTTP Cache issue. + // See crbug.com/1498203#c17. + fetchLater(url + "&method=GET&i=" + i); + fetchLater(url + "&method=POST&i=" + i, {method: 'POST'}); } `); - // Delete the iframe to trigger deferred request sending. document.body.removeChild(iframe); // The iframe should have sent all requests. await expectBeacon(uuid, {count: total}); -}, 'A discarded document sends all its fetchLater requests with default config.'); +}, 'A discarded document sends all its fetchLater requests.'); diff --git a/test/wpt/tests/fetch/metadata/portal.https.sub.html b/test/wpt/tests/fetch/metadata/portal.https.sub.html deleted file mode 100644 index 55b555a1b8e..00000000000 --- a/test/wpt/tests/fetch/metadata/portal.https.sub.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - diff --git a/test/wpt/tests/fetch/orb/resources/utils.js b/test/wpt/tests/fetch/orb/resources/utils.js index 94a2177f079..45fbc4cb38e 100644 --- a/test/wpt/tests/fetch/orb/resources/utils.js +++ b/test/wpt/tests/fetch/orb/resources/utils.js @@ -10,9 +10,92 @@ function contentTypeOptions(type) { return header("X-Content-Type-Options", type); } -function fetchORB(file, options, ...pipe) { - return fetch(`${file}${pipe.length ? `?pipe=${pipe.join("|")}` : ""}`, { - ...(options || {}), +function testFetchNoCors(_t, path, { headers }) { + return fetch(path, { + ...(headers ? { headers } : {}), mode: "no-cors", }); } + +function testElementInitiator(t, path, name) { + let element = document.createElement(name); + element.src = path; + t.add_cleanup(() => element.remove()); + return new Promise((resolve, reject) => { + element.onerror = e => reject(new TypeError()); + element.onload = resolve; + + document.body.appendChild(element); + }); +} + +function testImageInitiator(t, path) { + return testElementInitiator(t, path, "img"); +} + +function testAudioInitiator(t, path) { + return testElementInitiator(t, path, "audio"); +} + +function testVideoInitiator(t, path) { + return testElementInitiator(t, path, "video"); +} + +function testScriptInitiator(t, path) { + return testElementInitiator(t, path, "script"); +} + +function runTest(t, test, file, options, ...pipe) { + const path = `${file}${pipe.length ? `?pipe=${pipe.join("|")}` : ""}`; + return test(t, path, options) +} + +function testRunAll(file, testCallback, adapter, options) { + let testcase = function (test, message, skip) { + return {test, message, skip}; + }; + + const name = "..."; + [ testcase(testFetchNoCors, `fetch(${name}, {mode: "no-cors"})`, false || options.skip.includes("fetch")), + testcase(testImageInitiator, ``, options.onlyFetch || options.skip.includes("image")), + testcase(testAudioInitiator, `